전문가를 위한 파이썬 2.0

시퀀스

시퀀스는 순서가 있는 자료구조를 의미한다.

이 장에서는 표준 라이브러리에서 다루는 시퀸스, 즉 내장 시퀀스 중 일부[리스트, 튜플, 배열, 큐]에 대해 다룬다.

분류

시퀀스는 다음으로 분류할 수 있다.

  컨테이너 시퀀스 균일 시퀀스
의미 여러 자료형을 담을 수 있는 시퀀스 하나의 자료형만 담을 수 있는 시퀀스
list, tuple, collections, deque str, bytes, bytearray, memoryview, array.array
  가변 시퀀스 불변 시퀀스
의미 엘리먼트 대입 가능 엘리먼트 대입 불가(생성후 고정)
list, bytearray, array.array, collections, deque, memoryview tuple, str, bytes

지능형 리스트와 제너레이터 표현식

1
2
3
4
result = []
for item in sequence:
    if item > 10:
        result.append(item//2 + 32)

대부분의 사람이 알겠지만, 이는 다음과 같다

1
2
result = [item//2 + 32 for item in sequence if item > 10]
# 만약 조건이 없을 경우, if를 쓰지 않는다.

이를 Comprehension이라 하는데, 리스트를 만드므로 List Comprehension(LC)이라 부른다. 이를 똑같이 Set, Dict에 적용할 수 있는데, 이때는 SC, DC라 부른다.

위 아래를 비교해보면 알겠지만 훨신 간결하고 읽기 좋음을 알 수 있다. 하지만 세 줄이상인 경우, 코드를 분할하거나 for문을 사용하자.

반복문을 두개 이상 둘 수 있다. 다음 예를 보자.

1
2
3
4
5
6
7
result = []
for i in range(2):
    for j in range(3):
        result.append((i,j))

print(result)
# [(0,0), (0,1), (0,2), (1,0), (1,1), (1,2)]
1
2
3
print([(i,j) for i in range(2)
            for j in range(3)])
# [(0,0), (0,1), (0,2), (1,0), (1,1), (1,2)]
  • 파이썬에서 [], {}, ()안에서의 개행은 무시된다.

튜플

튜플은 불변 리스트이다. 하지만 불변 리스트의 위치별로 뭘 저장할지를 정해두면 레코드로서 작용한다.

1
2
3
4
5
6
7
8
data = [('서울',10000), ('부산', 3500), ('대구', 2500)]

for city_population in data:
    print("%3s: %5d명" % city_population)

# 서울: 10000명
# 부산:  3500명
# 대구:  2500명

여기서 이상한점이 보인다. city_population는 레코드(도시명, 인구수)인 튜플이다. "%3s: %5d명" % city_population는 어떻게 작용하는걸까? 반복형 언패킹에 대해 알아보자.

병렬 할당과 시퀀스 언패킹

시퀀스 언패킹은 시퀀스가 풀리는걸 의미하는데, 다음을 보자.

1
2
t = (1,2,3,4)
a,b,c,d = t

는 다음과 같다

1
2
3
4
a=t[0]
b=t[1]
c=t[2]
d=t[3]

대입연산(=) 의 왼쪽에 콤마로 구분된 변수들이 나오면(‘()’나 ‘[]’로 묶을 수 있다) 오른쪽의 시퀀스를 풀어 순서대로 대입한다. 이때 콤마로 구분한 것을 왼쪽에 두는 것을 병렬 할당이라 부르고, t가 저렇게 풀리는것을 시퀀스 언패킹이라 한다.

1
2
print("%s %s" % '1','2') # "%s %s" 각각에 풀려 들어감
a,b = b,a # swap

이때 풀리는 객체에 따라 리스트 언패킹, 튜플 언패킹이라 부른다.

1
_,a,b = input().split() # list unpacking

위 코드처럼 필요없는 항목에 대해서는 _를 더미처럼 사용하면 된다.

이 코드는 입력의 개수에 따라 오류가 나타난다.

만약 입력이 4개 들어오면?

1
ValueError: too many values to unpack (expected 3)

개수가 정해지지 않았을때는 *를 사용하여 초과한 항목을 잡을 수 있다.

1
2
3
4
5
6
7
8
*l, a, b = 1,2,3,4,5 # a = 4, b = 5, l = [1, 2, 3]
a, *l, b = 1,2,3,4,5 # a = 1, b = 5, l = [2, 3, 4]
a, b, *l = 1,2,3,4,5 # a = 1, b = 2, l = [3, 4, 5]
a, b, *l = 1,2,3     # a = 1, b = 2, l = []

a, b, c = 1,2,[9,8] # a = 1, b = 2, c = [9,8]

a, b, (c, d) = 1,2,[9,8] # a = 1, b = 2, c = 9, d = 8
  • *l 대신 *_로 버릴 수 있다.
  • *a는 객체에 하나만 있어야 한다. (모호해지기 때문)
  • 객체를 중첩하여 대입할 수 있다.

명명된 튜플

처음으로 돌아가 city_population예제를 보면 분명 튜플은 레코드의 역할을 할 수 있다. 그러나 각 값에 대한 레이블이 없는 것은 여전히 부족하다.

namedtuple이라는 레이블을 넣을 수 있는 튜플이 더 적합하다.

1
2
3
4
5
6
7
8
9
from collections import namedtuple
Population = namedtuple('Population', 'name population')

busan = Population('부산', 3500)

print(busan) # Population(name='부산', population=3500)

print(busan.name) # 부산
print(busan[0]) # 부산

정형화하면 다음과 같다.

1
2
from collections import namedtuple
TypeName = namedtuple('TypeName for __str__', 'attr1 attr2 attr3 ...')

namedtuple은 기존의 t[idx]를 통해서도 접근할 수 있고, t.attrname을 통해서도 할 수 있다.

1
2
3
TypeName._fields # ('attr1','attr2',...)
TypeName._make(('attr','attr',...) #  TypeName(attr1 = 'attr',attr2 = 'attr2',...)
busan._asdict() # [('name','부산'),('population',3500)]

다차원 슬라이싱

1
a[i,j]

NumPy와 같은 외부 패키지에서 2차원 슬라이스를 가져올때 사용한다. 이는 인덱스를 튜플로 받는다. a.__getitem__((i,j))

이때 i나 j 대신 slice로 1::3등을 넣을 수 있다.

또, 나머지 차원을 생략하는 경우, ‘…‘이라는 Ellipsis객체를 보낸다. 예를 들어, x[i,...]x[i,:,:,:]와 동일하다.

슬라이스에 연산하기

1
2
3
4
5
l = list(range(10))
l[2:5] = [2,3]
print(l) # [0,1,20,30,5,6,7,8,9]
del l[5:7]
print(l) # [0,1,20,30,5,8,9]

부분에 대한 할당을 할 수 있다.

병렬할당과는 엄연히 다른 문법이다. 슬라이스는 반드시 슬라이싱([::])이 들어가야하며 이는 값으로 평가되는것이 아니다.

1
2
3
4
5
6
>>> [a,b]=1,2 # 변수에 대한 할당이다
>>> a,b
(1, 2)
>>> [a,b][::]=2,1 # 슬라이싱을 하고 있으므로, 슬라이스에 할당이다.
>>> a,b
(1, 2)

대입(=) 왼쪽의 [::]는 연산이 아님을 기억하자.

시퀀스 곱, 덧셈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print([1,2,3] + [4,5,6]) # [1,2,3,4,5,6]

print([1,2] * 3) # [1,2,1,2,1,2]
board = [['_'] * 3] * 3 # *의 잘못된 사용, 리스트를 얕은 복사해 3개를 생성한다.
print(board) # [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]

board[1][2] = 'X'
print(board) # [['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]

# 대신 지능형 리스트를 사용하자
board = [['_']*3 for i in range(3)]

board[1][2] = 'X'
print(board) # [['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

sort와 sorted

일반적으로 사본을 만들지 않고 시퀀스를 변경하는 함수는 반환값이 None이다. 새 시퀀스를 생성하지 않았다는 의미.

  • list.sort()는 내부의 자료를 정렬하므로, None을 반환한다.
  • list.sorted()는 사본을 만들어 정렬한 후 반환한다.
  • sort, sorted 모두 팀 정렬을 이용한다.

정렬된 시퀀스를 bisect로 관리하기

bisect 모듈은 bisect()insort()함수를 제공한다. bisect()는 이진 탐색으로 인덱스를 가져오고, insort()는 정렬된 시퀀스안에 항목을 삽입한다.

각 함수는 lo, hi인자를 통해 검색 영역을 좁힐 수 있고, 디폴트는 lo = 0, hi = len(sequence)

이는 기본적으로 찾는 key보다 작거나 같은 것중 가장 큰것을 반환한다. 반대로 크거나 같은 것중 가장 작은것을 반환하는 것은 bisect_left이다. 다른 언어 기준으로 각각 lower_bound, upper_bound

바이트 코드 보기

1
2
import dis
dis.dis('문장')

을 통해 문장이 실제로 어떤 바이트 코드로 변환되는지 볼 수 있다.

메모리 뷰 사용하기

C의 union혹은 다른 타입으로의 포인터 케스팅과 같다.

1
2
3
4
5
6
7
8
numbers = array.array('h',range(-2,3)) # h는 2바이트 부호 있는 정수를 의미한다. (signed short, int16_t)
memv = memoryview(numbers)
print(len(memv)) # 5
print(memv[0]) # -2
memv_oct = memv.cast('B') # B는 1바이트 부호 없는 정수를 의미한다. (unsigned char,uint8_t)
print(memv_oct.tolist()) # [254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
memv_oct[5] = 4
print(numbers) # array('h', [-2, -1, 1024, 1, 2])