2부 데이터 구조체 - 2. 시퀀스

seokj·2023년 1월 6일
0

FluentPython

목록 보기
3/9

파이썬은 ABC의 영향을 많이 받은 언어이다. 시퀀스에는 문자열, 리스트, 바이트시퀀스, 배열, XML 요소, 데이터베이스가 있고 이는 모두 공통적으로 반복, 슬라이싱, 정렬, 연결 등의 연산을 적용할 수 있다.


중요 키워드
컨테이너 시퀀스, 균일 시퀀스, 가변 시퀀스, 불변 시퀀스
지능형 리스트, 제너레이터 표현식, 반복자 프로토콜
언패킹, tuple, collections.namedtuple, 병렬 대입 초과 항목 바인딩, 함수 인자 언패킹
:, ..., ellipsis, 슬라이싱, slice
bisect, insort
collections.deque, array.array, memoryview, queue 모듈, multiprocessing 모듈, asyncio 모듈, heapq 모듈, Queue, LifoQueue, PriorityQueue, JoinableQueue, heappush, heappop

덜 중요한 키워드
map, filter, ABC
_, gettext.gettext()
메서드 체이닝, 플루언트 인터페이스 스타일
itemgetter, attrgetter

참고자료
http://www.pythontutor.com/
http://en.wikipedia.org/wiki/Fluent_interface
https://docs.python.org/3/library/bisect.html
http://bit.ly/1Vm6WEa
https://peps.python.org/pep-0448/
http://bit.ly/py-pickle
http://bit.ly/1Vm6C8B


모든 시퀀스는 컨테이너 시퀀스균일 시퀀스로 나뉜다. 컨테이너 시퀀스는 다양한 자료형이 원소로 들어갈 수 있고 원소의 레퍼런스를 저장하는 반면 균일 시퀀스는 하나의 자료형만 원소로 들어갈 수 있고 원소 자체를 저장한다. 그래서 균일 시퀀스가 공간이 좀 더 작게 차지한다.
또한 모든 시퀀스는 가변 시퀀스불변 시퀀스로 나뉜다. 가변 시퀀스는 원소의 값을 변형시킬 수 있는 시퀀스이고 불변 시퀀스는 원소의 값을 변형시킬 수 없다.

시퀀스종류
컨테이너 시퀀스list, tuple, collections.deque
균일 시퀀스str, bytes, bytearray, memoryview, array.array
가변 시퀀스list, bytearray, array.array, collections.deque, memoryview
불변 시퀀스tuple, str, bytes

리스트를 생성할 때 for문으로 append하지 말고 지능형 리스트를 사용하자. for문은 다양한 작업을 처리할 수 있는 반면 지능형 리스트는 오직 리스트를 생성하기만 할 수 있기 때문에 가독성이 높아진다. 단, 지능형 리스트가 2줄 이상 길어질 때는 줄을 나누거나 for문을 사용하는게 낫다.

파이썬 3부터 지능형 리스트를 사용할 때 쓰는 []가 하나의 스코프를 구성하기 때문에 지능형 리스트에 쓰인 변수가 외부 변수를 수정하지 않는다.

(), {}, [] 안에서는 개행을 해도 의미상으로 무시되기 때문에 지능형 리스트나 제너레이터 표현식을 사용할 때 \ 없이 줄을 바꿔도 된다.

한 리스트 A가 있을 때 어떤 함수 f를 모두 통과시켜 새로운 리스트 B를 만들고 싶다고 하자. 지능형 리스트를 사용하는 것과 map, filter함수를 사용하는 것의 속도를 비교하면 다음과 같다.
1. f가 람다일 때 지능형 리스트
2. f에 상관없이 map, filter
3. f가 함수일 때 지능형 리스트

제너레이터 표현식은 지능형 리스트에서 [] 대신 ()를 쓴다는 점만 빼고 같다. 제너레이터 표현식은 리스트 이외의 시퀀스를 생성할 때 쓰인다. for문의 반복자에 바로 넣어도 된다. 제너레이터 표현식을 선언하는 것은 시퀀스를 미리 만들어두는 게 아니라 반복자 프로토콜을 이용해서 호출할 때마다 하나씩 원소를 얻을 수 있는 것이기 때문에 메모리에 저장해둘 필요가 없을 때 사용한다.


튜플은 레코드로서 기능을 한다. 정보의 위치에 따라 의미를 가지므로 순서를 잃으면 정보를 잃게 됨에 유의하자. 튜플은 언패킹이 가능하다. 언패킹할 때 *를 통해 초과된 인자를 받을 수 있고 이렇게 받았을 경우 결과로 리스트가 되어 나온다. 언패킹을 해서 안 쓰는 인자는 보통 _문자로 처리하지만 국제화된 소프트웨어를 만들 땐 일반적으로 gettext.gettext()함수에 대한 별명이 _로 이미 사용되고 있기 때문에 그 외의 경우만 언패킹 시 _를 사용하자. 함수에 인자를 전달할 때 튜플을 *로 언패킹하여 한 번에 전달할 수 있다. 언패킹할 때 구조만 맞으면 연쇄적으로 언패킹이 일어난다. 튜플 안의 튜플에 대해서도 언패킹 시 튜플 구조를 만들어주면 된다는 뜻.

네임드 튜플은 튜플의 각 위치에 이름을 넣을 수 있다. 일반적인 데이터 클래스는 __dict__에 필드명을 저장하는데 네임드 튜플은 그렇지 않으므로 좀 더 메모리를 절약할 수 있다. 네임드 튜플을 만들 땐 팩토리 함수에 네임드 튜플의 이름과 필드명을 입력하는데 필드명은 리스트로 넣어도 되고 띄어쓰기로 구분된 문자열 하나로 넣어도 된다. 네임드 튜플은 _fields를 통해 필드 명이 들어있는 튜플을 얻을 수 있다. 네임드 튜플은 _make함수를 통해 튜플을 네임드 튜플로 바꿀 수 있다. 튜플을 언패킹해서 네임드 튜플을 생성하는 것과 동일하다. 네임드 튜플은 _asdict를 통해 OrderedDict를 얻을 수 있다.

튜플은 불변 리스트로서의 기능도 한다. 리스트와 비교하여 추가, 수정, 삭제에 관련한 모든 메서드가 지원되지 않는다. 여기에 추가로 __reverse__함수도 지원되지 않는데, 이는 최적화 문제로 그렇게 되었다고 한다. reversed(튜플)을 사용할 수 있고 이때는 __reverse__함수가 사용되지 않는다.


모든 시퀀스에 []로 접근할 때 :를 사용하면 슬라이싱이 된다. 이때 slice객체가 생성되어 __getitem__이 호출되는 방식이다. 그래서 slice를 변수로 담아 저장해놓고 인덱싱할 때 사용하도록 해서 가독성을 높일 수도 있다.

슬라이싱 할 때 마지막 인덱스에 해당하는 원소를 범위에 포함하지 않는 이유는 여러 가지가 있다.
1. 시퀀스를 앞에서부터 원하는 크기로 자르기 쉽다. [:3]을 했을 때 얻을 수 있는 시퀀스의 크기가 3이다.
2. 구간의 길이 계산이 쉽다. [2:5]는 5-2로 계산하여 3이 된다.
3. 겹치지 않게 두 구간으로 나누는 것이 쉽다. [:3], [3:] 이런 식으로 같은 숫자를 넣으면 된다.

다차원 슬라이싱은 NumPy에서 ndarray.array객체가 __getitem__인자로 튜플을 받으면서 지원한다. 기본 파이썬은 지원하지 않는다. ...은 ellipsis타입 객체의 별명으로, 실제 사용할 때는 인스턴스화된 Ellipsis를 사용하고 생략을 의미하며 기본 파이썬에서는 활용되지 않는다. 사용자 정의 클래스를 사용하거나 NumPy에서 사용한다. NumPy에서는 arr[3,:,:,:] 이런 식으로 다차원 배열 인덱싱을 할 때 :,가 번갈아가며 반복될 때 ...로 고쳐서 arr[3,...]로 표기할 수 있다.

슬라이싱으로 참조된 리스트에 값을 대입할 수 있다. 이 값은 길이가 1이더라도 리스트여야 하고 stride가 1일 경우에는 대체되는 부분의 길이와 대체할 부분의 길이가 달라도 되지만 stride가 1이 아닐 경우에는 두 길이가 일치해야 한다. del으로 일부분만 제거할 수도 있다.


시퀀스에 +*연산을 적용할 수 있다. +는 이어붙이기, *는 횟수 반복의 의미를 가진다. +는 같은 종류의 시퀀스끼리만 붙일 수 있다. *는 시퀀스와 정수끼리 붙일 수 있다. 컨테이너 시퀀스인지 균일 시퀀스인지에 상관없이 *는 인스턴스를 새로 생성하지 않고 같은 객체를 가리키도록 하기 때문에 가변 객체가 원소로 있을 경우 문제가 될 수 있으니 유의하자.


  1. 복합할당 += 는 원자적인 연산이 아니다.
  2. 불변 시퀀스인 튜플에 가변 객체를 넣지 말자.
  3. dis를 통해 바이트 코드를 확인할 수 있다.
t = (1, 2, [30, 40])
t[2] += [50, 60]

위 코드를 실행하면 t[2]에 50, 60이 삽입되면서 오류가 난다. 매우 특이한 케이스. .append를 통해 오류 없이 원소를 넣을 수 있기도 하다.

import dis
dis.dis('s[a] += b')
#-결과-
  1           0 LOAD_NAME                0 (s)
              2 LOAD_NAME                1 (a)
              4 DUP_TOP_TWO
              6 BINARY_SUBSCR
              8 LOAD_NAME                2 (b)
             10 INPLACE_ADD
             12 ROT_THREE
             14 STORE_SUBSCR
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE

위 코드는 +=가 수행될 때 일어나는 연산이다. 10번 연산에서 '가리키고 있는 객체가 가변 객체일 경우 성공'한다. t[2]는 [30, 40]을 가리키고 있고 리스트는 가변 시퀀스이므로 성공. 그런데 14번 연산에서 '할당되는 대상이 가변 객체면 성공'한다. t가 튜플이고 불변 시퀀스이므로 실패. 따라서 t[2]는 변하고 t는 변하지 않지만 같은 것을 가리켜도 t[2]가 변했으므로 변한 값을 지니고 있는 것이다.


sorted는 리스트를 변형하지 않고 정렬된 리스트를 새로 만들어 반환한다. .sort는 원본 리스트를 변형하여 정렬시킨다. .sort와 같이 원본 리스트를 변형하는 함수는 관례적으로 None을 반환한다. .sort는 None을 반환하기 때문에 함수를 연속으로 연결할 수 없다. 이를 메서드 체이닝이라 하고 플루언트 인터페이스 스타일 기법 중 하나이다.


bisect는 이분탐색을 해주는 함수다. 시퀀스와 찾고자하는 값을 넣으면 인덱스가 반환된다. lo와 hi로 범위를 지정할 수 있고 파이썬 3.10부터는 key함수를 넣을 수도 있다. bisect는 bisect_right와 같은 함수인데 우선순위가 같은 원소끼리 있을 경우 제일 뒷쪽 인덱스를 반환하는 함수이다. bisect_left는 반대로 제일 앞쪽 인덱스를 반환한다. insort는 이분탐색으로 찾아서 삽입까지 해주는 함수다. 그런데 삽입이 선형시간이라 O(N)이 걸린다.


상황에 따라 리스트 말고 다른 시퀀스가 적합할 수 있다. 끝단에서 삽입, 삭제가 많이 일어나는 경우 collections.deque를 사용하자. 모든 원소가 C 자료형에 기본 자료형일 경우 array.array를 사용하자. 매우 큰 데이터를 다룰 때 데이터의 변형없이 원하는 크기 단위로 슬라이싱해서 보고 싶을 때 memoryview를 사용하자. memoryview는 NumPy 배열의 기본 틀이 되는 구조이기도 하다. 외부 라이브러리인 NumPy, SciPy, Pandas도 많이 쓰인다.

queue모듈에는 단일 프로세스에서 여러 스레드간의 실행 순서를 조절하기 위한 Queue, LifoQueue, PriorityQueue가 있다. multiprocessing모듈에는 여러 프로세스간의 여러 스레드 간의 실행 순서를 조절하기 위한 Queue, LifoQueue, PriorityQueue가 있고 추가로 JoinableQueue가 있다. asyncio모듈에는 비동기 상황에서 쓰일 Queue, LifoQueue, PriorityQueue, JoinableQueue가 있다. heapq에는 가변 시퀀스를 우선순위 큐로 사용할 수 있게 해주는 heappush, heappop함수가 있다.

profile
안녕하세요

0개의 댓글