시퀀스
문자열, 리스트, 바이트 시퀀스, 배열, XML요소, 데이터베이스 결과에는 모두 반복, 슬라이싱, 정렬, 연결 등 공통된 연산을 적용할 수 있다.
파이썬 표준 라이브러리는 C로 구현된 다음과 같은 시퀀스형을 제공한다.
컨테이너 시퀀스
서로 다른 자료성의 항목들을 담을 수 있는 list, tuple, collections.duque 형
균일 시퀀스
단 하나의 자료형만 담을 수 있는 str, bytes, bytearray, memoryview, array.array 형
균일 시퀀스가 메모리를 더 적게 사용지만 문자, 바이트, 숫자 등 기본적인 자료형만 저장할 수 있다.
시퀀스형은 가변성에 따라서도 구분할 수 있다.
가변 시퀀스
list, bytearray, array.array, collections.deque, memoryview 형
불변 시퀀스
tuple, str, bytes 형
지능형 리스트(listcomp) 는 가독성에 도움을 주며, 두 줄 이상 넘어가는 경우에는 for 문을 이용하는 것이 낫다.
참고) 파이썬에서는 [],{},() 안에서의 개행은 무시된다.
지능형 리스트로 할 수 있는 작업을 map() 과 filter() 함수를 이용해서도 구현할 수 있다. 속도를 비교해보면 아래와 같다.
import timeit
TIMES = 10000
SETUP = """
symbols = '$¢£¥€¤'
def non_ascii(c):
return c > 127
"""
def clock(label, cmd):
res = timeit.repeat(cmd, setup=SETUP, number=TIMES)
print(label, *('{:.3f}'.format(x) for x in res))
clock('listcomp :', '[ord(s) for s in symbols if ord(s) > 127]')
clock('listcomp + func :', '[ord(s) for s in symbols if non_ascii(ord(s))]')
clock('filter + lambda :', 'list(filter(lambda c: c > 127, map(ord, symbols)))')
clock('filter + func :', 'list(filter(non_ascii, map(ord, symbols)))')
listcomp : 0.010 0.009 0.009 0.009 0.009
listcomp + func : 0.013 0.013 0.014 0.014 0.013
filter + lambda : 0.013 0.013 0.012 0.012 0.012
filter + func : 0.012 0.013 0.012 0.012 0.012
결과를 보면 지능형 리스트가 가장 빠르고 코드도 가독성도 좋다.
튜플, 배열 등의 시퀀스 자료형을 초기화하려면 지능형 리스트를 사용할 수도 있지만, 반복자 프로토콜(iterator protocol) 을 이용하여 항목을 하나씩 생성하는 제너레이터 표현식(genexp)은 메모리를 더 적게 사용한다.
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes):
print(tshirt)
참고) 제너레이터 표현식이 함수에 보내는 단 하나의 인수라면 괄호 안에 또 괄호를 넣을 필요는 없다.
tuple(ord(symbol) for symbol in symbols)
또다른 기본 시퀀스형인 튜플은 레코드로서 사용 가능하며, 언패킹을 통해 병렬 할당도 가능하다.
lax_coordinates = (33.9425, -118408056)
latitude, longitude = lax_coordinates
travler_ids = [('USA', '31195885'), ('BRA', 'CE342567'), ('ESP', 'XPD394271')]
for passport in sorted(travler_ids):
print('%s/%s' % passport)
b, a = a, b
t = (20, 8)
print(divmod(*t)) # 몫과 나머지
import os
_, filename = os.path.split('~/etc_code/fluent_python/chapter_2.py')
print(filename)
> chapter_2.py
위의 코드에서 % 연산자는 튜플 언패킹으로 각 항목을 할당했다.
임시 변수를 사용하지 않고도 두 변수의 값을 교환할 수도 있다.
* 를 붙여 튜플의 언패킹도 가능하다. os.path.split() 함수는 경로와 파일명을 튜플 형태로 반환한다.
언패킹과 관련하여 * 를 사용하면 관심이 없는 일부 항목을 따로 할당할 수 있다.
a, b, *rest = range(5)
print(a, b, rest)
> 0 1 [2, 3, 4]
a, b, *rest = range(2)
print (a, b, rest)
> 0, 1, []
* 는 단 하나의 변수에만 적용할 수 있지만 어떠한 변수에도 적용 가능하다.
a, *body, c, d = range(5)
print(a, body, c, d)
> 0 [1, 2] 3 4
튜플 안에 내포된 튜플도 언패킹 가능하다.
metro_areas = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]
print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.'))
fmt = '{:15} | {:9.4f} | {:9.4f}'
for name, cc, pop, (latitude, longitude) in metro_areas:
if longitude <= 0:
print(fmt.format(name, latitude, longitude))
| lat. | long.
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
Sao Paulo | -23.5478 | -46.6358
참고) {:^n} 형태의 포매팅은 n이 문자열의 길이보다 큰 경우, n-문자열길이 만큼의 공백을 문자 앞 뒤에 할당한다.
collections.namedtuple() 함수는 필드명과 클래스명을 추가한 튜플의 서브클래스를 생성한다. 동시에 튜플과 동일한 크기의 메모리만 사용한다. 그 이유는 속성을 객채마다 존재하는 __dict__ 에 저장하지 않기 때문이다.
from collections import namedtuple
City = namedtuple('City', 'name country population coordinates')
tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
print(tokyo)
> City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 139.691667))
namedtuple 을 정의할 때는 클래스명과 필드명의 리스트 2개의 매개변수가 필요하다. 필드명의 리스트는 반복형 문자열이나 공백으로 구분된 하나의 문자열을 이용해서 지정한다.
추가로 몇 가지 속성을 더 가지고 있다.
print(City._fields)
> ('name', 'country', 'population', 'coordinates')
LatLong = namedtuple('LatLong', 'lat long')
delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.6312193, 77.208889))
delhi = City._make(delhi_data)
print(delhi._asdict())
> {'name': 'Delhi NCR', 'country': 'IN', 'population': 21.935, 'coordinates': LatLong(lat=28.6312193, long=77.208889)}
for key, value in delhi._asdict().items():
print(key, ":", value)
name : Delhi NCR
country : IN
population : 21.935
coordinates : LatLong(lat=28.6312193, long=77.208889)
_fields 속성은 필드명을 담고 있다.
_make() 메서드는 반복형 객체로부터 namedtuple 을 만든다. City(*delhi_data) 와 동일하다.
시퀀스 자료형에 대한 슬라이싱 기능을 알아본다.
s[start:stop:step] 에서 step 보폭만큼 항목을 건너뛴다. [::-1] 은 reverse 와 같은 동작을 한다.
s[start:stop:step] 는 seq.__getitem__(slice(start, stop, step))을 내부적으로 호출한다.
slice 함수는 슬라이스 구간에 이름을 붙일 수 있다.
_str = "6mm Tactile Switch x20"
sliced = slice(4, 10)
print(_str[sliced])
> Tactil
슬라이스에 직접 할당도 가능하다.
l = list(range(10))
print(l)
l[2:5] = [20, 30]
print(l)
del l[5:7]
print(l)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 20, 30, 5, 6, 7, 8, 9]
[0, 1, 20, 30, 5, 8, 9]
슬라이싱한 구간의 값이 동일한 개수만큼이 아니더라도 새롭게 할당한 값으로 바뀐다. 할당한 값이 더 적으면 전체 리스트의 길이가 늘어나고 반대는 줄어든다. 단, 할당되는 값은 반드시 반복 가능해야 한다. 즉, l[2:4] = 100 과 같은 방식은 불가능하다.
리스트 안에 리스트를 만드는 경우에는 주의해야할 점이 있다.
board = [['_'] * 3 for i in range(3)]
print(board)
board[1][2] = 'X'
print(board)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
weird_board = [['_'] * 3] * 3
print(weird_board)
weird_board[1][2] = 'X'
print(weird_board)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]
단순히 곱셈 연산자를 사용하면 내부 리스트가 모두 동일한 객체를 참조한다. 각 코드는 본질적으로 아래와 같이 동작한다.
row = ['_'] * 3
weird_board = []
for i in range(3):
weird_board.append(row)
board = []
for i in range(3):
row = ['_'] * 3
board.append(row)
동일한 리스트가 세 번 추가되는 것과 같다. 반면 지능형 리스트는 매번 새로운 리스트를 생성하는 차이가 있다.
시퀀스 자료형은 복합할당(+=, *=) 도 가능하다.
+= 연산자는 __iadd()__ 메서드가 호출된다. __iadd()__ 가 구현되지 않은 경우에는 자동적으로 __add__() 메서드가 호출된다.
예컨대 a = a+b 가 되어, a+b 먼저 수행되어 새로운 객체를 생성하고 이를 a 에 할당하는 방식이다. 따라서 객체의 정체성이 달라질 수 있다.
li = [1, 2, 3]
print(id(li))
> 4304699904
li *=2
print(id(li))
> 4304699904
복합할당의 경우, 연산 후에도 동일한 객체를 참조한다. 다만 튜플과 같은 불변 시퀀스는 이 연산을 수행할 수 없기 때문에 __add__() 메서드가 호출되어 다른 객체에 저장된다.
복합할당과 관련하여 다음과 같은 문제가 있다.
t = (1, 2, [30, 40])
try:
t[2] += [50, 60]
except TypeError:
print(t)
> (1, 2, [30, 40, 50, 60])
이 코드는 튜플 내에 가변 항목이 있는 경우 복합할당을 수행하면 어떤 결과를 나타내는지를 보여준다.
불변 시퀀스인 튜플는 항목 할당을 지원하지 않으므로 타입 에러가 발생한다. 근데 튜플의 값이 변했다.
s[a] += b 표현식에 대한 파이썬의 바이트코드를 보면 그 이유를 알 수 있다.
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 -> 1.
8 LOAD_NAME 2 (b)
10 INPLACE_ADD -> 2.
12 ROT_THREE
14 STORE_SUBSCR -> 3.
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
1. s[a] 값이 스택의 가장 위(Top Of Stack:TOS) 에 놓인다.
2. TOS += b 연산을 수행한다. TOS 가 가변 객체를 가리키면 연산은 성공한다.
-> 즉 t[2] = [30, 40] 이므로 연산이 성공한다.
3. TOS를 s[a] 에 할당한다. 이 때, s 는 불변 객체(튜플) 이므로 연산이 실패한다.
매우 드문 케이스이지만 이를 통해 알 수 있는 점은 아래와 같다.
- 가면 항목을 튜플에 넣는 것은 좋지 않다.
- 복합 할당은 원자적인 연산이 아니다. 즉 일부만 수행될 수 있다.
- 파이썬 바이트코드를 살펴보는 것이 내부 원리를 이해하는 데에 도움이 될 수 있다.
다음 정렬이다.
list.sort() 는 사본을 만들지 않고 리스트 내부를 변경한다. 그리고 None 을 반환한다. 반대로 sorted() 내장 함수는 새로운 리스트를 생성하여 반환한다. 이 둘 모두 reverse 와 key 라는 키워드를 인수로 받는다.
reverse 는 내림차순 여부이며 key 는 정렬에 사용할 키를 생성하기 위해 각 항목에 적용할 함수를 인수로 받는다.
정렬된 시퀀스는 bisect 모듈을 통해서 관리할 수 있다.
bisect(haystack, needle) 은 정렬된 시퀀스인 haystack 안에서 오름차순 정렬 상태를 유지한 채로 needle 을 추가할 수 있는 위치를 찾아낸다.
def grade(score, breakpoint=[60, 70, 80, 90], grades='FDCBA'):
i = bisect.bisect(breakpoint, score)
return grades[i]
print([grade(score) for score in [33, 99, 77, 70, 89, 90, 100]])
> ['F', 'A', 'C', 'C', 'B', 'A', 'A']
정렬은 매우 비싼 연산이다. 따라서 정렬을 유지하는 것이 좋다. bisect.insert() 함수가 이를 위해 만들어졌다.
insert(seq,item) 은 seq 를 오름차순으로 유지한 채로 item을 seq에 삽입한다.
import random
SIZE = 6
random.seed(1729)
my_list = []
for i in range(SIZE):
new_item = random.randrange(SIZE*2)
bisect.insort(my_list, new_item)
print('%d ->' % new_item, my_list)
10 -> [10]
0 -> [0, 10]
6 -> [0, 6, 10]
8 -> [0, 6, 8, 10]
7 -> [0, 6, 7, 8, 10]
2 -> [0, 2, 6, 7, 8, 10]
시퀀스 자료형 중에서 리스트를 가장 많이 사용한다. 가장 사용하기 편하기 때문이다. 그러나 상황에 따라 더 나은 자료형이 있을 수 있다. 예를 들어, 실수 천만 개를 저장할 때는 배열이 더 효율적이다. 그 이유는 기계가 사용하는 형태로 표현된 바이트 값만 저장하기 때문이다. 혹은 FIFO, LIFO 데이터 구조는 deque 가 더 빠르다.
배열은 숫자만 들어있는 경우에 훨씬 빨리 동작한다. 배열은 생성할 때, 타입코드를 지정한다. 예를 들어 signed char 에 대한 타입코드 b 를 지정하면, -128 ~ 127 까지의 정수로 해석된다. 숫자가 매우 많을 경우 많은 메모리가 절약된다. 또한 array.tofile(), array.fromfile() 메서드를 통해 매우 빠르게 배열을 저장하고 불러올 수 있다.
메모리 뷰(memoryview) 내장 클래스는 공유 메모리 시퀀스형으로서 PIL 이미지, SQLlite, NumPy 배열 등 데이터 구조체를 복사하지 않고 메모리를 공유할 수 있게 해준다.
collections.deque 는 큐의 양쪽 어디서든 빠르게 삽입 및 삭제할 수 있도록 설계된 thread-safe 양방향 큐다. popleft() 와 rotate() 처럼 고유의 메서드를 추가로 가지고 있다. 또한 append() 와 popleft() 메서드는 원자성을 갖고 있어, 멀티스레드 앱에서 락을 사용하지 않고도 deque 를 이용해 FIFO 큐를 구현할 수 있다.
그 외에도 표준 라이브러리 패키지에 있는 queue 모듈은 deque 와 달리 공간이 꽉 찼을 때, 항목을 버리지 않고 새로운 항목의 추가를 블로킹한다. 그리고 다른 스레드에서 큐 안의 항목을 제거해서 공간을 확보해줄 때까지 기다린다. 따라서 활성화된 스레드 수를 조절하기 좋다.
multiprocessing 모듈은 프로세스 간의 통신을 지원하기 위해 설계된 고유한 Queue 클래스를 구현한다.
asyncio 모듈은 비동기 프로그래밍 환경에서 작업을 관리하는 데 주안점을 둔다.
heapq 모듈은 가변 시퀀스를 힙 큐나 우선순위 큐로 사용할 수 있게 해주는 heappush() 와 heappop 등의 함수를 제공한다.
'python' 카테고리의 다른 글
Fluent Python (챕터 3) (0) | 2021.10.09 |
---|---|
Architecture Patterns with Python(1장) (0) | 2021.10.09 |
Fluent Python (챕터1) (0) | 2021.10.06 |
ChainMap (0) | 2021.05.07 |
array 로 생성, 저장, 로딩 (0) | 2021.05.02 |