5  시퀀스(Sequence)

파이썬에는 iterableiterator라는 개념이 많은 곳에서 사용된다. 그런데 파이썬을 처음 배우는 사람들에게 이런 추상적인 개념을 설명할 쉽게 설명할 자신이 없다. 이것을 알고 넘어가는 것이 무척 중요하다는 것은 알지만 그러하다.

그래서 추상적이지만 아주 추상적이지 않아서 어느 정도 구체적인 사례로 설명할 수 있는 sequence라는 개념을 가지고 시작한다. 시퀀스는 순서가 있는 iterable이다. 그러니까 iterable의 일종이고, 바꿔말하면 sequenceiterable의 부분집합이다.

이제 우리가 다룰 시퀀스가 어떤 개념에 속하는지 정리해 보자. 우선 정의를 보자.

5.1 시퀀스의 정의

파이썬 용어집에 따르면 sequence의 정의는 다음과 같다.

  • An iterable which supports efficient element access using integer indices via the __getitem__() special method and defines a __len__() method that returns the length of the sequence. Some built-in sequence types are list, str, tuple, and bytes. Note that dict also supports __getitem__() and __len__(), but is considered a mapping rather than a sequence because the lookups use arbitrary hashable keys rather than integers.

sequence보다 더 큰 집합인 iterable은 다음과 같이 정의할 수 있다.

  • Iterable: Any object that can be iterated over (e.g., lists, tuples, strings, dictionaries, sets).

SequenceIterable의 부분집합으로 그림 그림 8.1 같이 정리할 수 있다. Iterable에는 sequencenon-sequence로 나눌 수 있다.

그림 5.1: Iterables에는 Sequnece-type 객체와 Non-sequence type 객체가 있다.

5.2 Iterable에 속하면서 Sequence와는 다른 Mapping과 Set

iterable에는 sequencenon-sequence로 나눌 수 있는데, sequence와 다른 mappingset이 있으며, 쓰임새가 다르다. 이것은 아래와 같이 정리할 수 있다.

Sequence
값들이 정해진 순서를 가지고 있으며, 이 순서(인덱스)를 가지고 값을 꺼내올 수 있다. list, str, tuple이 대표적인 경우이다. - list: mutable - tuple: immutable - str: immutable
Mapping
파이썬 dictionary가 대표적인 경우, 키: 값 쌍으로 구성되어 있어서 어떤 값에 접근할 때 키(key)를 사용한다.
Set
파이썬 set이 대표적인 예로 수학에서 사용되는 집합과 같은 개념으로, 중복된 값을 가질 수 없고, 서로 다른 값들(unique)로 구성된다. 집합에 사용되는 연산을 적용할 수 있다.

다음 코드는 시퀀스에 속하는 리스트(list), 문자열(str), 튜플(tuple)에서 인덱스(순서)를 사용하여 값을 꺼내오는 예이다.

# 시퀀스-리스트, 문자열, 튜플 
l = [1, 7, 5]
s = "korea"
t = ("a", 1, True)
# 인덱스 0,  1, -1를 사용하여 접근
l[1], s[-1], s[0]
(7, 'a', 'k')

다음은 mapping인 딕셔너리(dictionary)에서 키(key)를 사용하여 값을 꺼내오는 예이다.

# 매핑: dictionary
d = {"a": 1, "b": 10, "c": print, "d": True}
d["a"], d["b"], d["c"], d["d"] 
(1,
 10,
 <function print(*args, sep=' ', end='\n', file=None, flush=False)>,
 True)

다음은 set에서 값을 꺼내오는 예이다. set은 중복된 값을 가질 수 없고, 서로 다른 값들로 구성된다.

# 셋: set 
s1 = {1, 3, 3, 5, 5, 8, 9}
s2 = set([1, 3, 3, 5, 5, 8, 9])
s1, s2
({1, 3, 5, 8, 9}, {1, 3, 5, 8, 9})

다음과 같은 리터럴과 생성자 함수를 사용하여 iterable 객체를 만들 수 있다.

  • 리스트: [], list()
  • 튜플: (), tuple()
  • 딕셔너리: {}, dict()
  • 셋: {}, set()
  • 문자열: 작은따옴표, 큰따옴표, 또는 triple, str()

빈 객체는 다음과 같이 만든다.

  • 리스트: [] 또는 list()
  • 딕셔너리: {}
  • 문자열: "", ''
  • 셋: set()
[], {}, "", set()
([], {}, '', set())

이런 빈 객체들은 불리언 맥락에서 False로 평가된다.

bool([]), bool({}), bool(""), bool(set())
(False, False, False, False)

Sequence가 파이썬 언어에서의 위치를 파악했으니까, 이제 스퀀스로 범위를 좁혀서 시퀀스를 가지고 어떤 연산을 할 수 있는지 알아보자.

5.3 Sequence에 대한 연산

Sequence에 대해서 다음과 같은 연산이 가능하다. 이것들을 익히고 나면 개별 문자열, list, tuple 등과 같은 타입을 이해하고 다루는 것이 간단해진다. 그래서 이 연산들은 암기하여 숙지할 필요가 있다. 설령 처음에 암기하고 반복하면 나중 파이썬 공부가 훨씬 수월해질 것이다.

예를 들어 튜플 언팩킹(tuple unpacking) 방법이 있다. 이것은 튜플에 속하는 값들을 하나씩 가져오는 방법을 말한다.

x, y, z = (1, 2, 3)
print(x, y, z)
1 2 3

이렇게 튜플 언팩킹을 이해했는데, 또 공부하다 보니 리스트 언팩킹(tuple unpacking)이라는 것도 나온다. 비슷하다.

x, y, z = [1, 2, 3]
print(x, y, z)
1 2 3

또 공부하다 보니 iterable 언팩킹이라는 것도 나온다. 이건 더 일반화된 개념이다.

x, y, z = range(1, 4)
print(x, y, z)
1 2 3

이렇게 개별적으로 공부하는 것보다는 시퀀스/iterable은 모두 이런 연산을 지원한다고 이해하고 나서, 개별 데이터 타입에 대해 접근하는 것이 훨씬 쉽다는 생각이 들어 이 장을 정리한 것이다.

5.3.1 멤버십을 확인하는 in 연산자

Sequence는 여러 개의 item으로 구성된다. 어떤 객체가 해당 sequence에 포함되어 있는지 확인할 때는 in 연산자를 사용한다.

s = [98, 78, 81, 63, 55]
t = tuple('abc')
78 in s, "a" in t
(True, True)

문자열 Sequence인 경우에는 문자열을 구성하는 서브 문자열이 포함되어 있는지를 체크할 수 있다.

my_country = "우리나라 아름다운 대한민국"
"대한민국" in my_country
True

5.3.2 인덱싱

obj[index] 문법으로 사용하여 위치 값을 가지고 값을 가지고 올 수 있다. 인덱스는 항상 0으로 시작한다(zero-based index). 끝에서 시작하는 경우에는 -1부터 시작한다.

s = [98, 78, 81, 63, 55]
s[0], s[-2]
(98, 63)
t = ('abc', 87, 65, "M", True)
t[0], t[-1]
('abc', True)
intro = "My name is John Does.\n"
print(intro)
intro[0], intro[-1]
My name is John Does.
('M', '\n')

5.3.3 슬라이싱(slicing)

슬라이싱은 obj[start:stop:step] 문법을 사용하여 일련의 값을 가지고 오거나 또는 그 위치에 새로은 슬라이스로 된 값을 넣을 수 있게 해준다.

  • start는 inclusive
  • stop은 exclusive

start, stop, step는 빈 상태로 둘 수 있는데, 빈 채로 두면 다음과 같은 의미를 가진다.

  • start를 빼면, obj[:stop]로 처음부터라는 의미
  • stop을 빼면, obj[start:]로 끝까지는 의미
  • step을 빼면, ‘하나씩 차례대로’ 의미를 가진다.
my_list = list(range(1, 11))
my_list
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
my_list = list(range(1, 11))
my_list[2:5]
[3, 4, 5]

만약 startstop을 빼면, 모두를 의미하는 데, 주로 값을 복사하는 데 이용한다(shallow copy).

my_list = list(range(1, 11))
new_list = my_list[:]
new_list
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

여기서 show copydeep copy의 개념을 알고 가자.

  • shallow copy: 값을 복사
  • deep copy: 원래 객체를 그대로 복사하는 것임. Reference가 바뀌지 않음.
    • 먼저 deep copy는 그 자체를 복사하는 것이기 때문에 새롭게 복사된 객체를 바꾸면 원래의 객체도 바뀐다.
# deep copy
my_list = list(range(1, 11))
new_list = my_list # deep copy 
new_list[0] = 100
print(my_list)
print(new_list)
[100, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[100, 2, 3, 4, 5, 6, 7, 8, 9, 10]

다음은 shallow copy로 값만을 복사해서 새로운 객체를 만든다. 만들어진 객체는 원래의 객체와는 독립적으로 존재하기 때문에, 만들어진 객체의 값을 바꿔도 원래 객체는 변하지 않는다. list.copy() 메서드를 써도 되지만, 슬라이싱을 이용하는 경우도 많다.

# shallow copy: 값만 복사 
my_list = list(range(1, 11))
new_list = my_list.copy() # list.copy() 메서드로 
# new_list = my_list[:]도 같은 효과
new_list[0] = 100
print(my_list)
print(new_list)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[100, 2, 3, 4, 5, 6, 7, 8, 9, 10]

start는 inclusive, stop은 exlusive이기 때문에, 다음과 같은 방법으로 obj[:값], obj[값:] 문법을 사용하여 2개로 쪼갤 때 편리하다.

my_list = list(range(1, 11))
# 이것을 앞 3개와 나머지로 나누는 방법 
head, others = my_list[:3], my_list[3:]
print(head)
print(others)
[1, 2, 3]
[4, 5, 6, 7, 8, 9, 10]

step에 음의 정수를 사용할 수도 있다. 이런 경우에는 뒤에서 앞으로 이동한다. 따라서 이 기능을 활용하여 값을 거꾸로 뒤집을 때 편리하게 사용된다.

my_list = list(range(1, 11))
my_list[::-1]
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

슬라이싱에 대하여 값을 할당하거나 del 문을 사용하여 값을 삭제할 수 있다. 값을 할당할 때는 슬라이싱 부분을 좌변에 놓는다.

my_list = list(range(1, 11))
my_list
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

my_list 객체의 처음 3개의 아이템의 값을 11, 22, 33으로 바꾸고자 하면 다음과 같이 할 수 있다.

my_list[:3] = [11, 22, 33]
my_list
[11, 22, 33, 4, 5, 6, 7, 8, 9, 10]

다음은 my_list의 4, 5, 6 값을 삭제한다.

del my_list[3:6]
my_list
[11, 22, 33, 7, 8, 9, 10]

좌변과 우변의 개수가 달라도 된다. 그래도 해당 부분의 값이 바뀐다.

my_list[1:3] = [100]
my_list
[11, 100, 7, 8, 9, 10]

단, = 우변에는 항상 iterable이 와야 한다. 위 코드에서 my_list[1:3] = 100라고 하면 오류가 발생한다.

my_list[1:3] = 100
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[25], line 1
----> 1 my_list[1:3] = 100

TypeError: must assign iterable to extended slice

슬라이싱 연산은 연산의 대상이 되는 데이터의 타입을 항상 유지시킨다.

my_tuple = tuple(range(1, 11))
my_tuple[1:2]
(2,)

5.3.4 range() 함수

range(start, stop, step) 함수는 정수로 된 시퀀스를 만들 때 사용되는데, 슬라이싱과 유사한 문법을 가지고 있다. 또 그런데 이 함수는 값을 바로 만들지 않고, range라는 객체를 반환하고, 이 객체를 활용하여 값을 만든다. 이렇게 하는 이유는 메모리 효율성을 높이기 위한 것으로, iterator라는 개념과 연결된다.

range(10) # 0에서 9까지 
range(0, 10)

이것을 구체적인 값으로 바꿀 때 list() 함수를 많이 사용한다.

list(range(1, 10)) # 1부터 9까지, 리스트 
[1, 2, 3, 4, 5, 6, 7, 8, 9]

tuple() 함수에 넣으면 튜플이 된다.

tuple(range(10, 0, -1))
(10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

반면 for/in 루프는 내부에 각각의 값을 반환하는 장치가 마련되어 있다(iterator protocol). 따라서 range() 함수가 반환하는 것을 실제 값으로 구체화하지 않아도 바로 사용할 수 있다.

for i in range(5):
    print(i)
0
1
2
3
4

5.3.5 zip(), enumerate() 함수

zip() 함수는 여러 시퀀스에 있는 아이템들을 해당 위치에 있는 것끼리 모아서 튜플로 변환한 iterable을 만든다. range() 함수처럼 실제 값을 바로 반환하지 않는다. for문 등과 같이 iterator protocol를 사용하여 계산에 사용된다.

list1 = [1, 2, 3, 4]
list2 = ["a", "b", "c", "d"]
zip(list1, list2)
<zip at 0x15d5ea240>
list1 = [1, 2, 3, 4]
list2 = ["a", "b", "c", "d"]
print(list(zip(list1, list2)))
print(tuple(zip(list1, list2)))
for item in zip(list1, list2):
    print(item)
[(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]
((1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'))
(1, 'a')
(2, 'b')
(3, 'c')
(4, 'd')

enumerate() 함수는 시퀀스의 각 아이템에 인덱스를 부여해 준다. for/in 문은 인덱스 없이 그대로 값을 사용하는데, 그 위치값(index)이 필요한 경우에 사용한다.

my_list = ["a", "b", "c", "d"]
list(enumerate(my_list))
[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd')]

5.3.6 개수, 최솟값, 최댓값, 합계, 포함 여부

Sequence에 들어 있는 아이템의 개수는 len() 함수를 사용하여 구한다.

l = [1, 7, 5, 6, 3]
s = "korea is"
t = ("a", 1, True, [1, 2, 3])
len(l), len(s), len(t)
(5, 8, 4)

최솟값, 최댓값은 min(), max() 함수를 사용하여 구한다.

  • 하나의 iterable
l = [1, 7, 5, 6, 3]
min(l), max(l)
(1, 7)
  • 여러 개의 값들을 여러 인자로 줄 수도 있다.
print(max(1, 7, 5, 6, 3))
print(min(1, 7, 5, 6, 3))
7
1

sum() 함수는 합계를 계산하는 데, 이 함수에는 값들을 하나의 iterable로 주어야 한다.

sum(range(1, 101))
5050

어떤 값이 주어진 sequence의 항목에 존재하는지, 즉 그 membership을 확인할 때는 x in s 문법을 사용한다. 포함하지 않는지를 확인할 때는 not in을 사용한다.

1 in list(range(5))
True
10 not in list(range(10))
True

5.3.7 Concatenation과 반복

시퀀스에 +를 사용하면 두 객체가 결합되어 하나의 새로운 객체가 된다. s * n을 사용하면 값들이 반복하여 만들어진다.

[11, 22, 33] + [44, 55]
[11, 22, 33, 44, 55]
("a", "b", "c") + ("d", "e")
('a', 'b', 'c', 'd', 'e')
[11, 22, 33] * 3
[11, 22, 33, 11, 22, 33, 11, 22, 33]
("a", "b", "c") * 3
('a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c')

5.3.8 Unpacking

시퀀스 등에서 값을 개별 변수들로 풀어 내는 방법이다. 좌변과 우변의 개수가 맞아야 한다. 이렇게 하는 것을 병렬 할당(parallel assignment)라고도 한다.

# list를 unpacking
a, b, c  = [1, 3, 5]
print(a, b, c, sep=", ")
1, 3, 5
# tuple을 unpacking
a, b, c, d = (1, 3, 4, 5)
print(a, b, c, d, sep=", ")
1, 3, 4, 5

어떤 함수가 tuple을 반환하는 경우, 이와 같은 방법으로 값을 받아서 사용한다.

q, r = divmod(19, 3)
q, r
(6, 1)

만약 어떤 값들은 할당하고, 나머지들을 하나의 리스트로 묶을 수도 있다. 이 경우 리스트에 들어갈 것을 *을 사용한다.

a, b, c, *d = range(1, 101)
print(a, b, c)
print(d)
1 2 3
[4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]
f1, f2, f3, *_, l3, l2, l1= range(1, 101)
print(f1, f2, f3, l3, l2, l1)
1 2 3 98 99 100

_도 하나의 변수이기는 한데, Python 사용자들은 앞으로 사용하지 않을 값을 담을 때 주로 사용한다.

range() 함수의 결과를 list()tuple() 등과 같은 함수를 통해서 실제 값을 만들지 않아도 upacking이 되고 있다. 즉, 이 의미는 unpacking에도 for문을 사용할 때 처럼 iterator protocol이 들어 있음을 의미한다.

Unpacking은 함수의 인자(argument)를 줄 때도 편리하게 사용된다.

def f(a, b, c):
    return a + b + c

k = (1, 2, 3)
f(*k)
6

Unpacking은 for 문에서도 유용하게 사용된다. 이 구문도 암기해야 한다.

my_list = [(1, "a"), (2, "b"), (3, "c")] # a list of tuples
for n, c in my_list:
    print(c + str(n))
a1
b2
c3

5.3.9 시퀀스 정렬(sort)

시퀀스에 포함된 아이템을 정렬하는 문제를 생각하자.

list.sort() 함수: list는 mutable 객체이다. 이 메서드는 해당 리스트의 메모리에서(in-place) 포함된 아이템을 정렬한다. reverse=True라는 인자를 사용하여 내림차순으로 정렬할 수 있다.

my_list = [1, 3, 5, 2, 5, 7, 8, -1]
print(id(my_list))
5852970304
my_list.sort() 
print(my_list)
print(id(my_list))
[-1, 1, 2, 3, 5, 5, 7, 8]
5852970304
my_list.sort(reverse=True)
print(my_list)
print(id(my_list))
[8, 7, 5, 5, 3, 2, 1, -1]
5852970304

파이썬 내장 함수 sorted()는 아무 sequence(iterable)에 대하여 사용할 수 있다. list.sort() 함수는 list가 mutable 특성을 가진 점을 이용한다. 시퀀스에서는 tuple처럼 immutable인 경우도 있기 때문에, mutability에 구애받지 않고 사용할 수 있게 하기 위해서 in-place 방식이 아닌 새로운 객체를 만드는 방식으로 작동하며, 결과는 list로 반환한다. reverse 인자는 앞의 list.sort() 함수와 같다. 그러니까 sorted() 함수는 임의의 iterable을 받아서 정렬한 다음 그 결과를 list로 반환한다.

my_list = [1, 3, 5, 2, 5, 7, 8, -1]
print(id(my_list))
5852945408
my_list = sorted(my_list)
print(my_list)
print(id(my_list))
[-1, 1, 2, 3, 5, 5, 7, 8]
5861569088

다음은 tuple에 적용한 예이다.

my_tuple = (1, 3, 5, 2, 5, 7, 8, -1)
sorted(my_tuple)
[-1, 1, 2, 3, 5, 5, 7, 8]

sorted() 함수에 key=fn 이라는 키워드 인자를 사용하여 단순한 값이 아닌 함수 fn의 결과로 만들어지는 값을 기준으로 정렬이 가능하다. 이 기능은 매우 강력하다.

fruits = ['grape', 'raspberry', 'apple', 'banana']
fruits
['grape', 'raspberry', 'apple', 'banana']

이 객체를 sorted() 함수에 넣으면 유니코드(Unicode) 코드 포인트 순서대로 정렬된다.

sorted(fruits)
['apple', 'banana', 'grape', 'raspberry']

fruits 리스트에 들어 있는 값들의 문자열 길이에 따라서 정렬하려면 다음과 같이 key에 문자열의 길이를 구하는 len() 함수를 지정한다. 끝에 ()가 없다. 함수가 실행된 값을 전달하는 것이 아니라 함수 자체를 전달한다.

sorted(fruits, key=len)
['grape', 'apple', 'banana', 'raspberry']

key=fn에 전달할 수 있는 함수는 하나의 인자를 취해서 하나의 값을 반환하는 함수라면 다 괜찮다. 사용자 정의 함수도 넣을 수 있다.

다음 my_fn() 함수는 단어를 받아서 거꾸로 쓴 결과를 반환한다. 이 함수를 key 인자에 주었다.

def my_fn(word):
    return word[::-1]

# 거꾸로 쓴 단어들 
for word in fruits:
    print(my_fn(word))

# 이 함수를 key에 넘김
sorted(fruits, key=my_fn)
eparg
yrrebpsar
elppa
ananab
['banana', 'apple', 'grape', 'raspberry']

복잡한 객체를 대상으로 정렬할 때 key 인자를 사용할 때 도움이 되는 함수가 operator 모듈에 존재한다.

operator.itemgetter() 함수는 아이템을 선택하는 기능을 제공한다. 다음 예를 보자. 다음 예는 리스트의 아이템이 딕셔너리 구조를 가지고 있는 students 객체가 있는데, 내부의 딕셔너리의 age 키를 기준으로 정렬하기 위해서 operator.itemgetter('age')key 인자에 적용했다.

import operator

# 딕셔너리로 구성된 리스트 
students = [
    {'name': 'John', 'age': 25},
    {'name': 'Jane', 'age': 22},
    {'name': 'Dave', 'age': 24},
]

# age를 기준으로 정렬 
sorted_students = sorted(students, key=operator.itemgetter('age'))

sorted_students
[{'name': 'Jane', 'age': 22},
 {'name': 'Dave', 'age': 24},
 {'name': 'John', 'age': 25}]

다음은 아이템의 튜플인 경우이다. 튜플인 경우 키를 사용할 수 없기 때문에 operator.itemgetter(1)와 같이 인덱스를 사용하여 정렬 기준을 잡았다.

import operator
metro_data = [
     ('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)),
     ('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]
sorted(metro_data, key=operator.itemgetter(1))
[('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
 ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
 ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
 ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
 ('New York-Newark', 'US', 20.104, (40.808611, -74.020386))]

operator.itemgetter()와 비슷한 함수로 operator.attrgetter()가 있다. 이 함수는 어떤 객체의 인스턴스를 attribute 값을 가지고 정렬할 때 사용된다.

import operator

# 클래스
class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f'Student(name={self.name}, age={self.age})'

# 인스턴스
students = [
    Student('John', 25),
    Student('Jane', 22),
    Student('Dave', 24),
]

sorted(students, key=operator.attrgetter('age'))
[Student(name=Jane, age=22),
 Student(name=Dave, age=24),
 Student(name=John, age=25)]

이와 같이 sorted() 내장 함수는 리스트 뿐만 아니라 임의의 iterable를 받을 수 있고, key 인자를 사용하여 다양한 상황에서 정렬과 관련된 다양한 기능을 수행할 수 있는 다재다능한 도구이다.

5.3.10 컴프리헨션(comprehension)

[x를사용계산코드 for x in s] 문법을 사용하여 새로운 리스트를 만드는 방법이다. xs의 아이템을 하나씩 가지고 온다.

[n**2 for n in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

List comprehension은 리스트를 만든 데 사용되는데, 비슷한 방법으로 딕셔너리, 셋도 만들 수 있다. 좀 더 일반화된 generator expression을 만들 수 있다. 따라서, 반드시 알아야 하는 문법이다.

다음은 코드를 보자.

{c: i for i, c in enumerate("abcd")}
{'a': 0, 'b': 1, 'c': 2, 'd': 3}
  • "abcd"는 문자열로, sequence의 하나이다.
  • enumerate() 함수는 각 아이템에 인덱스를 붙여 준다. 이 함수를 호출하여 iterable을 반환한다.
  • 이것을 for문 안에서 unpacking을 적용하여 인덱스, 문자 쌍으로 나눈다.
  • 전체는 dictionary comprehension으로 딕셔너리를 만든다.

5.3.11 Generator expression

리스트 컴프리헨션은 리스트를 만드는 문법인데, 비슷한 문법을 일반적인 경우로 확장한 것이 generator expression이다. () 안에 리스트 컴프리헨션의 문법과 같이 코딩한다.

이와 같은 방법을 사용하는 이유는 리스트와 같이 실제 값을 만들어 사용하는 것보다 메모리 효율성이 높아진다. 왜냐하면 generator object는 필요한 값들을 하나씩 만들기 때문이다.

# 1에서 19까지 숫자들을 제곱하는 generator expression
squares_generator = (n**2 for n in range(1, 11))
squares_generator
<generator object <genexpr> at 0x15cb012f0>

Generator expression은 generator object를 만든다. 이 객체는 iterator protocol을 갖추고 있다.

for n in squares_generator:
    print(n)
1
4
9
16
25
36
49
64
81
100
list((n**2 for n in range(1, 11)))
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
sum((n**2 for n in range(1, 11)))
385

이와 같이 함수 () 안에 generator expression이 단독 인자로 사용되는 경우는 ()를 생략할 수 있다.

list(n**2 for n in range(1, 11))
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
sum(n**2 for n in range(1, 11))
385

5.4 정리

이 장에서는 (str, list, tuple 등을 포함하는) 파이썬 시퀀스의 개념과 시퀀스를 가지고 할 수 있는 연산을 정리하였다.