[Python] list comprehension에 대한 즐거운 이해
0. Index
- 들어가며
- 단어의 해석에 대해서
- list comprehension 기본 문법
- 3.1. 일반적인 배열과 리스트의 활용
- 3.2. 문법 살펴보기
- 추가적인 활용
- 4.1. 조건문으로 필터링하기
- 4.2. 자잘한 활용
- 다른 자료구조로의 확장
- 마치며
- 자료 출처
1. 들어가며
간만에 파이썬 포스트다. 전에 썼던 파이썬 포스트에 어느 분이 도움이 많이 됐다고 댓글을 남기셨다. 그래서 기분이 좋아서 파이썬 포스트를 쓰게 된 것도 있다. 생각해보면 나는 파이썬을 정말 좋아하고 자신도 있으면서 알고리즘 등에 비해 파이썬 포스트를 거의 쓰지 않은 것 같다.
오늘 포스트는 파이썬에서 기본이라고 할 수 있는 list comprehension(이하 ‘리스트 컴프리헨션’)에 대해 다룬다. 기본적인 문법에서부터 자잘하고 꽤나 까다로운 활용법, 그리고 이름이 리스트
컴프리헨션이지만 리스트뿐 아니라 다른 built-in 내장 구조에도 문제없이 쓰일 수 있음을 확인할 생각이다.
리스트 컴프리헨션에 대해 다루는 것은 사실 이 주제가 내게 매우 재밌기보다는 곧 내 마음속 숙제로 남아있었던 Iterable, Iterator, Generator에 관해 포스트를 쓸 예정이기 때문이다. 개인적으로 파이썬에 대해 최소한의 이해를 하고 있다고 말하려면 이 셋에 대해 문제없이 구분하고 활용할 수 있어야 한다고 생각한다. 항상 이 주제를 포스트로 쓰고 싶은 마음이 있었는데 미루고 미루다 결국은 쓰게 될 것이다. 리스트 컴프리헨션은 이중 특히 Generator와 관련 있기 때문에 먼저 다루면 좋겠다고 생각했다. 또 만약 파이썬 입문자라면 다른 언어에는 없는 파이썬의 독특한 이 문법은 반드시 잘 알고 적재적소에 활용할 수 있어야 한다.
그리고 개인적으로 파이썬에 매력을 느끼고 집중적으로 공부하게 된 계기가 list comprehension 때문이기도 하다. 그러기에 꼭 다른 포스트를 위한 선행학습이 아니더라도 독자적으로 다루는 것이 의미가 있다고 느낀다.
2. 단어의 해석에 대해서
리스트 컴프리헨션은 영어로 list comprehension
이라고 쓴다. 이때 list야 우리가 아는 리스트일 때, ‘comprehension’라는 단어가 주의를 끈다. 항상 말하지만 개념을 다룰 때 이름의 의미를 먼저 짚고 넘어가야 한다. 이 단어는 무슨 뜻일까? 사전에서 찾아보면 ‘(특히 언어적인) 이해력‘을 의미한다고 한다. 예전 TEPS 시험을 공부할 때도 독해력 시험을 ‘Reading comprehension’이라고 했던 것이 생각난다. 근데 다음 장에서 문법을 살펴보겠지만, 리스트 컴프리헨션이 딱히 무슨 ‘이해력’을 연상시키지 않는다.
그래서인지 한글로 번역한 파이썬 서적들에서 리스트 컴프리헨션을 ‘리스트 표현식’, ‘리스트 조건식’과 같이 초월번역한 것을 봤다. 나는 이런 번역을 한국인에게 comprehension이라는 단어가 실제 개념과 매치되지 않기에 초보자들을 이해시키기 위한 노력의 발로라고 해석했다. 하지만 나는 이 포스트에서 저 두 단어를 쓰지 않을 생각이다. 표현식은 ‘expression’을 연상시킨다. 또 조건식은 조건문을 연상시키는데 리스트 컴프리헨션에 if
statement를 쓸 수는 있지만 경험상 안 쓰는 경우가 더 많다. 다시 생각해보니 ‘표현식’이라는 번역은 그럴듯한데 나는 쓰지 않겠다. 나는 ‘expression’의 의미를 프로그래밍을 3년 하고 깨달았다. 내가 아는 한 저 단어의 의미를 초보자에게 가르치는 코딩 교육은 없었기에.
말이 길었는데 결론은 이 포스트에서는 별다른 번역 없이 ‘리스트 컴프리헨션’이라는 단어를 그대로 사용한다.
3. list comprehension 기본 문법
3.1. 일반적인 배열과 리스트의 활용
리스트 컴프리헨션은 쉽게 말해 ‘리스트를 쉽게, 짧게 한 줄로 만들 수 있는 파이썬의 문법’이다. 이게 핵심이다. 리스트는 Java 등 타 언어의 ‘배열’과 매칭되는 선형 자료구조로서 프로그래밍에서 매우 자주 활용하는 중요한 요소다. 예를 들어 자바에서 크기가 10인 배열을 생성하고, 각 원소에 인덱스에 2를 곱한 값을 할당하는 코드를 짠다고 하자. 코드는 다음과 같을 것이다.
int size = 10
int[] arr = new int[size];
for (int i = 0; i < arr.length; i++) {
a[i] = i * 2;
}
자바를 잘 모른다면 이 코드는 정확히 예로 제시한 일을 한다고만 이해하면 된다. 똑같은 작업을 파이썬에서 해보자.
size = 10
arr = [0] * size
for i in range(len(size)):
arr[i] = i * 2
아하, 이제 이해가 된다. arr 에는 0, 2, 4, …, 18까지의 값이 담겨있을테다.
이게 타 언어와 파이썬에서 배열(또는 리스트)을 만드는 일반적인 과정이다. 이 짧은 코드에서는 크게 두 가지 작업이 차례로
진행된다.
- 배열을 선언한다
- 선언한다는 것은 배열의 크기를 정하고 배열을 특정 이름의 변수에 할당하는 것을 포함한다. 자바 같은 언어에서는 추가로 원소들이 어떤 자료형(위에서는 정수)인지까지도 선언한다.
- 배열의 각 원소에 값을 할당한다
이때 리스트 컴프리헨션은 이 작업을 매우 간편하게 해주는 역할을 한다. 다음 절에서 확인해보자.
3.2. 문법 살펴보기
앞선 절에서 배열 또는 리스트를 만들 때는 선언과 할당 작업이 순차적으로 일어나기 때문에 이런 간단한 작업에서 최소 3, 4줄의 공간이 필요함을 확인했다. 이때 리스트 컴프리헨션에 대해 내가 제시한 정의를 기억해보자. ‘리스트를 쉽게, 짧게 한 줄로 만들 수 있는 파이썬의 문법’ 이때 특히 짧게 한 줄로
가 중요한 것 같다. 보통 배열을 만들고 사용할 때는 선언과 할당을 같이 하고, 선언만 하거나 할당만 하는 경우는 없기 때문에 이 작업을 간편하게 한 줄로 해결하는 것이 리스트 컴프리헨션이다.
리스트 컴프리헨션의 기본적인 문법은 다음과 같다.
[ ( 변수를 활용한 값 ) for ( 사용할 변수 이름 ) in ( 순회할 수 있는 값 )]
이 문법은 앞선 예제의 파이썬 버전과 비교해보면 좀 더 쉽게 이해할 수 있다. 배열을 만들고 for 반복문 안에서 각 원소의 값을 할당하는 작업이 이 한 줄로 일어난다. 괄호 안의 이름들을 조금 설명해보겠다.
- 변수를 활용한 값
- 이는 앞선 코드의 ‘i * 2’에 대응된다. 배열의 각 원소에 값을 할당하는데 이 예제처럼 인덱스에 2를 곱해 할당할 수도 있고, 경우에 따라서는 제곱을 해서 할당할 수도 있다. 이렇게 ‘for’ 앞에는 실제로 할당될 값을 적는다.
- 사용할 변수 이름
- 앞선 코드의 ‘i’에 대응된다. 파이썬의 for 문에서 ‘for’와 ‘in’ 사이에 for 블락 안에서 사용될 인자의 이름을 쓰는 것과 같다. ‘i’ 대신에 ‘n’ 등 어떤 단어를 써도 된다.
- 순회할 수 있는 값
- 여기서 ‘순회할 수 있는 값’은 range 처럼 값을 하나씩 살펴볼 수 있는 것을 총칭한다. range 뿐 아니라 다른 리스트, set, dict, tuple 등도 될 수 있다. 일반 for문에서 쓸 수 있는 모든 값이 들어갈 수 있는 것이다. 참고로 이런 것들에는 ‘Iterable’이라는 고귀한 이름이 붙어있다. 향후 관련 포스트에서 만나볼 수 있을 것이다.
이제 리스트 컴프리헨션을 통해 앞선 예제를 한 줄로 만들어보자.
size = 10
arr = [i * 2 for i in range(size)]
print(arr)
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
눈부시게 간단하다. 선언과 할당이 한 줄에 이뤄지기 때문에 이렇게 극단적으로 짧아질 수 있었다.
다른 예제를 만들어보자. 지금 만든 arr 에서 각 원소를 제곱해서 새로운 리스트를 만들어보자. 물론 이것도 한 줄로 만들 수 있다.
# 방금 만든 arr을 사용
new_arr = [n * n for n in arr]
print(new_arr)
[0, 4, 16, 36, 64, 100, 144, 196, 256, 324]
‘in’ 뒤에는 순회할 수 있는 모든 것이 들어올 수 있기 때문에 리스트도 문제없이 들어올 수 있다. 그리고 각 원소의 이름을 ‘n’으로 설정한 뒤 실제 각 원소는 ‘n’의 제곱으로 만들어 할당했다. 이때 좋은 점은 arr 을 그대로 사용해서 같은 크기의 리스트를 만들기 때문에 리스트의 크기를 len 함수로 알아내지 않아도 만들 수 있다는 점이다.
예제를 하나만 더 만들어보자. 문자열(str)을 입력 받아서 각 문자를 두 개 이어붙인 새 문자열을 원소로 하는 리스트를 만들고 싶다고 하자. 가령 ‘가나다’에 대해 이 작업을 하면 결과는 [‘가가’, ‘나나’, ‘다다’]와 같을 것이다. 코드는 다음과 같다.
word = '가나다'
print([c * 2 for c in word])
['가가', '나나', '다다']
의도한 대로 나왔다. 알다시피 파이썬에서는 문자열을 한 글자씩 순회가능하다. 다시 말해 리스트처럼 인덱싱이 가능하고, for 문에서 순회할 수 있다. 따라서 이런 예제도 얼마든지 가능했다.
세 가지 예제를 살펴봤는데 문법이 결코 어렵지 않다. 이런 기본적인 예제를 굳이 더 만들지 않아도 될 것같은 확신이 들 정도다. 이 다음은 개인이 써보면서 익숙해져야 하는 문제라고 생각한다.
다음 장에서는 리스트 컴프리헨션을 보다 까다롭게 사용하는 예제를 살펴보자.
4. 추가적인 활용
4.1. 조건문으로 필터링하기
앞서 살펴본 기본예제에서는 리스트 컴프리헨션으로 생성되는 리스트의 크기는 순회하는 값에 맞게 자동으로 결정된다는 것을 확인했다. new_arr의 크기가 arr 의 크기에 맞게 자동으로 결정된 것을 보면 알 수 있다.
하지만 리스트 컴프리헨션은에 if
조건문을 써서 특정 값을 걸러내서, 즉 필터링(filtering)하여 생성되는 리스트의 원소로 넣거나 뺄 수 있다. 문법은 기본 리스트 컴프리헨션의 끝에 ‘if’ 문을 넣기만 하면 되서 바로 매우 간단한 예제를 만들어서 이해하자.
예를 들어 1부터 10까지의 값에서 짝수만 원소로 갖는 리스트를 만들고 싶을 때는 다음과 같이 입력한다.
size = 10
arr = [n for n in range(1, 11) if n % 2 == 0]
print(arr)
[2, 4, 6, 8, 10]
‘if’문을 지우고 리스트 컴프리헨션을 쓰면 1부터 10까지의 값을 갖는 크기 10의 리스트가 생성되었을 것이다. 하지만 맨 뒤에 ‘if’를 사용해서 해당 조건문에서 참이 나온 값만 배열의 원소가 되도록 값을 필터링했다. n 은 range를 통해 1부터 10까지 순회했을 것이다. 이때 저 if 문을 만족하는 값은 짝수, 2, 4, …, 10 뿐이기 때문에 홀수는 제외됐다.(exclude) 이렇게 리스트 컴프리헨션에서 조건문을 통해 특정 값들을 필터링할 수 있다.
여기서 좀 재밌고 이해가 안 가는 내용이 나온다. 조건문을 여러 개 쓸 수는 없을까?
가령 1부터 30까지의 값을 순회해 리스트를 만드는데
- 2의 배수이고(AND) 3의 배수인 수만 필터링하거나,
- 2의 배수이거나(OR) 3의 배수인 수만 필터링하고 싶을 수 있다.
먼저 AND 조건은 가능하다. 코드는 다음과 같이 역시 한 줄로 짤 수 있다.
arr = [n for n in range(1, 31) if n % 2 == 0 if n % 3 == 0]
print(arr)
[6, 12, 18, 24, 30]
어떤 수가 2의 배수이고 동시에 3의 배수라는 얘기는 결국 6의 배수라는 말과 같다. 1부터 30까지 순회하면서 6의 배수의 자연수만 추렸다. 즉, AND 조건식은 리스트 컴프리헨션을 통해 사용가능하다. 근데 식을 자세히 보자. 두 if 문 사이에 and
statement가 없다. ‘and’ 없이 조건문을 나열하면 이들은 모두 AND 조건으로 계산된다는 것을 알 수 있다. 그런데 놀랍게도 사이에 명시적으로 ‘and’를 써주면 SyntaxError가 뜬다.
arr = [n for n in range(1, 31) if n % 2 == 0 and if n % 3 == 0]
SyntaxError: invalid syntax
매우 기묘하고 이해가 안 간다. 왜 이렇게 동작할까? ‘and’를 안 쓰는 문법보다는 명시적으로 적는 문장이 가독성 측면에서 더 바람직하지 않나? 이건 지금도 잘 모르겠다.
반대로 다중 if 조건문에 대한 OR 연산은 아예 안 된다. 명시적으로 or
연산자를 입력하면 SyntaxError가 발생하고 쓰지 않으면 AND로 해석된다. 따라서 이런 경우에는 if 문을 여러 개 쓰지 말고, 한 if 문에서 ‘or’ 연산자로 논리 연산을 묶어줘야 한다.
arr = [n for n in range(1, 16) if n % 2 == 0 or n % 3 == 0]
# 한 if 문 내에서 or 연산 해결
print(arr)
[2, 3, 4, 6, 8, 9, 10, 12, 14, 15]
4.2. 자잘한 활용
이번에는 조금 까다롭고 자잘한 리스트 컴프리헨션 활용방법을 살펴볼까 한다. 리스트 컴프리헨션의 가장 애매한 부분으로 이 부분은 파이썬 언어에서 일관성의 부족으로 많이 비판받는 부분이기도 하다.
리스트 안에 리스트를 넣어 다차원의 리스트를 만들 수 있다. 가령 다음과 같이 2차원 매트릭스를 만드는 예는 자주 사용된다.
arr = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[10, 11, 12],
]
print(len(arr))
print(len(arr[0]))
4
3
리스트 arr 은 2차원 매트릭스로 4개의 행, 3개의 열을 가지고 있다. 즉, \(4 X 3\) 행렬이라고 말할 수 있다. 이번 절에서 살펴볼 예제는 이 리스트를 활용해 만들자.
먼저 차원 축소를 해보자. 현재 arr 은 2차원 배열인데 이 배열의 차원을 축소해 1차원으로 환원한다. 즉 1부터 12까지의 원소를 포함한 단일 리스트로 만든다는 뜻이다. 코드는 다음과 같다.
flat_one = [n for row in arr for n in row]
print(flat_one)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
조금 머리가 어질해진다. for 반복문이 두 개 쓰였는데 먼저 행을 다루는 for문이 나오고, 뒤에 행 안의 셀을 다루는 for문이 나온다. 이 순서를 기억하기가 헷갈릴 수 있다. 힌트는 flat_one 이라는 1차원 리스트를 일반 for 문을 중첩해서 만든다고 생각하는 것이다. 코드는 대략 다음과 같을 것이다.
flat_one2 = []
for row in arr:
for n in row:
flat_one2.append(n)
print(flat_one2)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
여기서 행을 제어하는 for 문 안에 단일 셀을 다루는 for문이 등장했다. 리스트 컴프리헨션에서도 그 순서대로 for문이 등장했다고 이해하자.
다음은 단순 데이터 조작이다. 앞서서는 리스트의 구조 자체를 비틀었다면 이번에는 구조(차원)는 유지하면서 각 원소의 값을 제곱한다고만 하자. 어떻게 해야 할까?
squared_list = [[n ** 2 for n in row] for row in arr]
print(squared_list)
[[1, 4, 9], [16, 25, 36], [49, 64, 81], [100, 121, 144]]
\(4 X 3\) 행렬 구조를 유지하면서 각 셀의 값을 단순히 제곱한 결과가 나왔다. 이때 리스트 컴프리헨션의 구조는 앞선 예의 정반대다. 즉 각 셀의 값을 다루는 for문 뒤에 행을 다루는 for문이 등장했다. 처음에 나는 이 둘이 정말 헷갈렸다. 이를 어떻게 이해해야 하는거지?
이 예제를 이해하는 힌트는 [n ** 2 for n in row] 이 부분을 A
라는 임의의 값으로 치환해보는 것이다.
squared_list = [A for row in arr]
이렇게 바꾸면 이 리스트 컴프리헨션은 우리가 처음에 살펴본 리스트 컴프리헨션의 기본문법과 정확히 일치한다. 이런 단일값으로의 치환은 수학에서 일반화를 위해 자주 사용하는 기법이기도 하다.
위의 두 예제가 충분히 헷갈릴 수 있다. 만약 이해가 잘 안 된다 하더라도 크게 좌절할 필요없다. 파이썬을 4년 써보지만 알고리즘 풀 때 제외하고는 다차원 리스트 컴프리헨션을 다루는 경우는 별로 없다. 특히 2차원을 넘어가는 리스트 컴프리헨션을 써본 기억은 정말 손에 꼽는데 기억나는 예제는 꽤 어려운 동적 계획법 문제를 풀 때 한 번 써본 것 같다. 즉 for 문을 중첩하는 리스트 컴프리헨션은 꽤 자잘하고(miscellaneous) 몰라도 일반 반복문 중첩을 통해 무난히 해결할 수 있으니 ‘이런 게 있다’고 넘어가도 괜찮다.
5. 다른 자료구조로의 확장
지금까지 리스트 컴프리헨션(list comprehension)으로 리스트
만을 다뤘다. 만약 내가 블로그를 잘 써서 이 개념이 꽤나 유용하다는 것이 여러분에게 설득됐다면 여러분들은 이런 질문을 던질 수도 있을 것이다. 컴프리헨션 식으로리스트뿐 아니라 다른 내장 자료구조인 set, tuple, dict도 만들 수는 없을까? 충분히 제기할 수 있는 질문이고, 실제로 리스트 컴프리헨션 식으로 tuple, set, dict도 만들 수 있다. 이럴 때는 그냥 컴프리헨션
으로 부르는 것이 더 바람직하겠다. 리스트에 한정되지 않는 보다 일반적인 개념으로서 말이다.
그래서 이번 장에서는 컴프리헨션식으로 set, dict, tuple을 만들어보자. 기본적인 문법은 같기 때문에 하나씩 만들기만 하면 될 것 같다.
먼저 set
을 만들자. set은 생성 기호를 []
대신 {}
를 사용한다.
set_boy = {n ** 2 for n in range(10)}
print(set_boy)
{0, 1, 64, 4, 36, 9, 16, 49, 81, 25}
0부터 9까지의 정수를 제곱한 값들을 갖는 set을 컴프리헨션 식으로 만들었다. 생성 문법은 기호만 바뀌고 나머지는 리스트를 생성할 때와 같다. 집합의 특성답게 값의 순서가 일정치않다는 것을 눈여겨볼만 하다.
자잘하지만 이 얘기를 하고 넘어가자. 내가 이 예제를 즉석에서 만들었는데, 처음에는 0부터 9까지의 값을 그대로 갖는 집합을 생성했다가 방금 제곱한 값을 갖도록 수정했다. 왜 굳이 그랬을까? 그것은 이런 간단한 예제는 컴프리헨션을 쓰지 않고도 만들 수 있기 때문이다.
print(set(range(10)))
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
단순히 range(10) 를 set 클래스 생성자에 입력만해도 의도한 값이 나온다. 이렇게 만들 수 있는 예제에는 컴프리헨션이 필요하지 않다. 보다 복잡하고 까다로운 연산이 필요할 때 컴프리헨션이 빛을 발한다.
다음으로 dict
. dict는 set과 같이 같은 {}
기호를 사용해 생성한다. 그럼 컴프리헨션 식으로 만들었을 때 어떻게 set인지, dict인지 구별할까? 생각보다 간단하다. for문 앞에 key: value
쌍을 써주고, 사용하는 변수를 두 개로 두면 된다.
dict 컴프리헨션으로 다음과 같은 dict를 만들자. 키는 ‘a’부터 ‘z’까지 갖고 각 키는 1부터 26까지의 값을 갖도록. 나라면 이렇게 만들겠다.
from string import ascii_lowercase as LOWERS
dict_boy = {c: n for c, n in zip(LOWERS, range(1, 27))}
print(dict_boy)
{'a': 1, 'b': 2, 'c': 3, ..., 'x': 24, 'y': 25, 'z': 26}
코드를 조금 설명하면 내장 모듈 string 에는 각종 상수 문자열들이 들어있는데 여기서 영어 소문자 상수를 가져왔다. 모듈 안에는 각 상수의 이름을 소문자로 할당해놨는데 as
로 대문자화해 사용하는 것을 기억하자. 이런 상수값을 ‘a’부터 ‘z’까지 다 입력하는 한심한 짓은 하면 안 된다고 누누히 강조한다. 프로그램의 요구사항이 바뀌어서 소문자 대신 대문자가 key가 된다면 다시 다 입력할 것인가? 할 수야 있겠지만 매우 고통스러울 것으로 예상된다. 매직 넘버와 매직 스트링은 피하는 것이 상책이다.
그리고 컴프리헨션을 통해 의도한 dict를 만들었다. 이때 내장함수 zip 을 써서 key와 value로 사용할 값들의 묶음을 병렬로 배치했다. 각 for문에서 c 는 소문자와 매칭되고, n 은 자연수와 매칭될 것이다.
사실 각 key와 value를 추가 조작없이 dict로 만드는 간단한 컴프리헨션은 아까 set 생성자를 썼던 것과 같이 이렇게 써도 된다.
print(dict(zip(LOWERS, range(1, len(LOWERS)+1))))
{'a': 1, 'b': 2, 'c': 3, ..., 'x': 24, 'y': 25, 'z': 26}
정확히 같은 값이 출력된다. 하지만 각 key에 숫자 자체가 아닌 제곱한 값 등을 써야한다면 컴프리헨션 식이 바람직할 것이다.
확인할 것은 아까와 달리 range 안에 27이라는 명시적인 숫자 대신 \(len(LOWERS) + 1\)라고 입력했다는 것이다. 이렇게 되면 코드는 더 유연해진다. 프로그램의 요구사항이 또 바뀌여서 각 한글 글자에 1부터 총 글자수만큼 숫자를 배정하라는 요구가 올 수도 있는 것 아닌가. 이것도 매직 넘버를 개선한 좋은 예다.
다음으로 tuple. tuple은 리스트와 비슷한데 생성 기호를 []
대신 ()
를 쓴다. 언뜻 보기에는 이 생성기호를 사용한 컴프리헨션 식으로 tuple을 만들 수 있을 것 같다.
1부터 9까지의 값을 차례로 갖는 tuple을 컴프리헨션으로 만들어보자.
tuple_boy = (n for n in range(1, 10))
print(tuple_boy)
<generator object <genexpr> at 0x7f1ac8177db0>
띠용? tuple이 담겼으리라 예상됐던 우리의 tuple_boy 에는 전혀 예상못한 값이 들어있고 무슨 제너레이터(generator)라고 되어 있다. 앞서 list, dict, set을 만들 때와 같이 컴프리헨션을 사용했는데 왜 tuple만 생성되지 않았지? 그나저나 제너레이터는 대관절 무엇이란 말인가?
일단 제너레이터는 향후 다룰 Iterable, Iterator, Generator의 마지막 요소 Generator를 말한다. 쉽게 말해 우리가 앞서 작성한 식은(tuple_boy) 제너레이터를 생성하는 generator comprehension이다. 그리고 제너레이터는 어떤 일련의 값을 연속해서 반환할 예정이 되어 있는 순회 객체라고 표현하겠다. 이는 일단은 넘어가자. 위의 세 개념은 한 포스트를 할애할 가치가 있을 정도로 중요하다. 일단은 컴프리헨션 식에 감싸는 기호를 ()
를 쓰면, tuple이 아닌 다른 값이 나온다고 알고 있자.
그럼 컴프리헨션 식을 사용해 tuple을 만들 수는 없다는 뜻일까? 상식적으로 그럴리는 없다. 나름 파이썬의 가장 중요한 4개의 자료구조 중 하나 아닌가. 다음과 같이 입력해보자.
tuple_boy = tuple(n for n in range(1, 10))
print(tuple_boy)
(1, 2, 3, 4, 5, 6, 7, 8, 9)
tuple 자료구조에 생성자에 괄호 없는 컴프리헨션 식을 집어넣었다. 결과 의도한 대로 tuple이 생성된 것을 확인할 수 있다. 이렇게 함수나 클래스 생성자에 생성 기호 없는 컴프리헨션을 사용하는 예는 향후 Iterable, …를 다루는 포스트에서 보다 상세히 살펴봐야 한다.
이렇게, 리스트 컴프리헨션을 일반화해서 tuple, set, dict도 컴프리헨션을 통해 만들 수 있다는 것을 확인했다.
6. 마치며
리스트 컴프리헨션에 대한 내용을 마쳤다. 리스트 컴프리헨션(list comprehension)은 배열을 생성할 때 일반적으로 작성하는 선언, 할당(초기화)의 순차적인 작업을 일원화해 짧고 쉽게 리스트를 생성할 수 있는 파이썬의 독특한 문법으로, 프로그래밍에서의 배열의 중요성을 생각해봤을 때 꽤나 유용한 문법이다. 리스트 컴프리헨션을 사용해 리스트를 만드는 방법을 살폈고 if 문을 통해 원소 필터링이 가능하다는 것도 확인했다. 추가로 2차원 이상의 리스트 컴프리헨션식도 가능하다는 것을 봤다. 하지만 복잡하고 매우 실효성이 있지는 않아서 참고만 해도 괜찮다. 마지막으로 리스트 컴프리헨션이 리스트에만 국한되지 않고 tuple, set, dict에도 똑같이 적용해 자료구조를 생성할 수 있음을 봤다.
리스트 컴프리헨션이 사실 별 것도 아닌데 적다보니 길어졌다. 한 200줄이면 끝낼 수 있을 줄 알았는데. 포스트를 마치고 나니 제가 알고 있다고 해서 개념을 혹시 대충 설명했나 하는 의혹이 듭니다. 길이에 비해 내용이 알차지 않다거나, 내용 중 설명이 미흡한 부분이 있다면 댓글로 남겨주시면 감사하겠습니다.
이상 포스트를 마칩니다.