Skip to content
This page has been auto-translated and may contain errors.View in English

람다와 컴프리헨션

이 세 가지 기능에는 공통점이 있습니다. 원래는 여러 줄이 필요했을 아이디어를, 읽기 쉬운 한 줄의 표현식으로 표현할 수 있게 해줍니다. 잘 쓰면 코드가 더 짧고 명확해지지만, 잘못 쓰면 읽기 힘든 코드가 됩니다. 이 장에서는 각각을 언제 써야 하고, 언제 멈춰야 하는지를 다룹니다.

람다, 컴프리헨션, zip은 흔한 패턴을 표현식으로 압축해주는 세 가지 도구입니다. 반드시 써야 하는 것은 아니지만, 파이썬 코드 곳곳에 등장하므로 알아보고 능숙하게 쓸 수 있어야 합니다. 원칙은 이렇습니다: 코드가 짧아지는 것만이 아니라 의도가 더 명확해질 때 사용하세요.

이 도구들은 모두 변형 과정을 표현식(값으로 평가되는 코드 조각)으로 나타냅니다. 결과를 한 단계씩 조립하는 명령형 루프를 쓰는 대신 말이죠. 람다는 이름 없는 작은 함수를 인라인으로 만들어줍니다. 컴프리헨션은 컬렉션을 한 번에 만들어냅니다. 제너레이터는 지연 평가(값을 한꺼번에 만들지 않고 필요할 때마다 만드는 방식)를 하기 때문에, 전체 시퀀스가 아니라 항목 하나만 메모리에 유지합니다. zip은 여러 시퀀스를 나란히 순회합니다. 이렇게 하면 코드가 "어떻게 조립할지"가 아니라 "무엇을 원하는지"를 읽어내는 방식이 되지만, 한 줄짜리 코드가 읽기 힘들어지기 전에 멈춰야 한다는 조건이 붙습니다.

람다 함수

람다는 이름이 없고 표현식 하나로만 이루어진 함수입니다. lambda 키워드로 만듭니다. 람다의 진짜 쓸모는, 이름 있는 함수를 따로 정의하지 않고 필요한 자리에 바로 인라인으로 쓸 수 있다는 점입니다. 이 특성 덕분에 sorted()와 함께 쓸 때 유용합니다.

람다는 이름 없는 단일 표현식 함수입니다. 여러 인자를 받을 수 있지만, 본문은 문장(statement)이 아니라 반드시 표현식 하나여야 합니다. 주로 인라인 key= 인자나 콜백으로 쓰이며, 이런 자리에 완전한 def를 쓰면 오히려 불필요한 간접 표현이 됩니다. 그보다 복잡해지면 def를 쓰세요.

람다는 def가 만드는 것과 동일한 종류의 함수 객체이지만, 세 가지 제약이 있습니다. 이름이 없어서 트레이스백(오류가 발생했을 때 파이썬이 출력하는 오류 보고)에 <lambda>로 표시되고, 본문이 표현식 하나뿐이라 문장을 담을 수 없으며, docstring도 가질 수 없습니다. 실제로 디버깅 시간을 잡아먹는 함정은 클로저(자신을 둘러싼 스코프의 변수를 캡처하는 함수)에서 발생하는 **지연 바인딩(late binding)**입니다. 루프 안에서 만든 람다는 루프 변수를 참조로 캡처하기 때문에, 모든 람다가 람다가 만들어질 때의 값이 아니라 변수의 최종 값을 보게 됩니다:

python
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])        # [2, 2, 2], [0, 1, 2]가 아님

funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])        # [0, 1, 2]: i=i가 지금 이 값을 고정함

i=i 트릭은 생성 시점의 현재 값을 기본 인자로 고정시킵니다. 루프 안에서 콜러블을 만들 때는 언제든 이 방법을 쓰세요.

python
double = lambda x: x * 2
double(5)   # 10

다음과 동일합니다:

python
def double(x):
    return x * 2

대부분의 경우 def를 쓰세요. 람다에는 진짜 장점이 하나 있습니다: 이름을 붙이지 않고 필요한 자리에 바로 인라인으로 쓸 수 있다는 점입니다. 이 특성 덕분에 sorted(), map(), filter()와 함께 쓸 때 유용합니다:

python
players = [("민준", 87), ("서연", 74), ("지호", 92)]

sorted(players, key=lambda p: p[1])              # 점수 기준 오름차순 정렬
sorted(players, key=lambda p: p[1], reverse=True)  # 점수 기준 내림차순 정렬

람다가 없다면, key= 인자를 위해서만 이름 있는 함수를 정의해야 합니다. 람다는 의도를 그 자리에 국한시켜 눈에 보이게 유지해줍니다.

람다는 여러 인자를 받을 수 있습니다:

python
add = lambda a, b: a + b
add(3, 4)   # 7

람다를 써야 할 때: 한 곳에서만 쓰이는 작은 표현식일 때만 쓰세요. 점점 커지거나 재사용이 필요하다면 제대로 된 def를 작성하세요. 여러 연산자에 걸쳐 있거나 조건문이 필요한 람다는 대개 def로 바꿔야 한다는 신호입니다.

Juno람다 함수 람다는 이름이 없는, lambda 키워드로 쓰는 한 줄짜리 작은 함수입니다. 존재 이유는 딱 하나, sorted()에 쓸 key=로 인라인으로 들어가서, 한 가지 일만 하는 함수를 따로 정의하지 않게 해주는 것입니다. 깔끔한 표현식 한 줄을 넘어서는 순간 저는 진짜 def를 씁니다. 그게 마음이 편합니다.
Juno람다 함수 람다는 이름 없는 단일 표현식 함수로, 이름 있는 def를 쓰면 오히려 잡음만 늘어나는 자리에서 인라인 key=나 콜백으로 쓰기 좋습니다. 본문은 표현식 하나뿐이고 문장은 허용되지 않습니다. 조금이라도 복잡해지면 def로 가세요.
Juno람다 함수def와 같은 함수 객체지만 이름, 문장, docstring이 없으니 한 줄짜리 key= 용도로만 쓰세요. 함정은 지연 바인딩입니다. 루프 안에서 만든 람다는 루프 변수의 최종 값을 읽으므로, lambda i=i: i로 값을 고정해서 쓰세요. 이건 누구나 한 번은 당합니다.

리스트 컴프리헨션

파이썬에서 가장 흔한 변형 작업은 이렇습니다: 시퀀스를 가져와 각 항목에 어떤 작업을 하고, 새로운 리스트를 돌려받는 것입니다. 리스트 컴프리헨션은 이 작업을 읽기 쉬운 한 줄로 해냅니다: [expression for item in iterable]. if로 필터를 추가할 수도 있습니다.

리스트 컴프리헨션은 루프로 리스트를 만드는 패턴을 간결하게 대체합니다. 일반적으로 .append()를 쓰는 동등한 for 루프보다 빠른데, 매번 메서드를 호출하지 않기 때문입니다. 구조는 [expression for item in iterable if condition]이며, if 절은 선택 사항입니다.

컴프리헨션은 매번 .append()를 호출하는 같은 루프보다 확실히 빠르므로, 변형을 통해 리스트를 만드는 관용적인 방법입니다. 또한 자체 스코프(이름이 보이는 영역)에서 실행되므로, 루프 변수가 주변 코드로 새어나가지 않습니다. [n ** 2 for n in numbers] 이후에는 n이라는 이름이 컴프리헨션 바깥에 존재하지 않습니다. 주의할 함정은 본문이 실제로 뭔가 무거운 일을 하는 컴프리헨션을 쓰는 경우입니다. 표현식이 깔끔한 변형을 넘어서거나 로깅이나 try 블록을 넣고 싶어진다면, 명시적인 루프가 더 나은 도구입니다. 컴프리헨션은 표현식만 담을 수 있고 문장은 절대 담을 수 없기 때문입니다.

긴 방법:

python
numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
    squares.append(n ** 2)

리스트 컴프리헨션:

python
squares = [n ** 2 for n in numbers]

구조는 항상 같습니다: [expression for item in iterable].

python
scores = [87, 42, 96, 55, 71]
scaled = [s * 1.1 for s in scores]          # 10% 보너스 적용
as_grades = [f"{s}/100" for s in scores]    # 각 항목을 포맷
Juno리스트 컴프리헨션[expression for item in iterable]은 시퀀스를 가져와 각 항목에 한 가지 일을 하고, 새로운 리스트를 돌려줍니다. 왼쪽에서 오른쪽으로 읽으면 하는 일이 그대로 보입니다. 저는 이 기능을 처음 봤을 때, 다른 언어를 번역하는 게 아니라 진짜 파이썬을 쓰고 있다는 느낌을 처음 받았습니다.
Juno리스트 컴프리헨션[expression for item in iterable]은 루프로 만들고 .append()하는 패턴을 대체하며, 그러면서도 더 깔끔하게 읽힙니다. 새로운 리스트를 만들 뿐, 아무것도 변경하지 않습니다. 표현식을 단순하게 유지하세요. 그러지 않으면 얻은 가독성이 다시 사라집니다.
Juno리스트 컴프리헨션.append() 루프보다 빠르고 자체 스코프에서 실행되므로 루프 변수가 밖으로 새지 않습니다. 기억할 원칙: 컴프리헨션은 표현식을 담을 뿐 문장을 담지 못하므로, try 블록이나 로그 한 줄이 필요해지는 순간 명시적인 루프가 이깁니다.

조건으로 필터링하기

if 절을 추가하면 테스트를 통과한 항목만 포함시킬 수 있습니다. 결과는 조건이 True인 항목만 담긴 새 리스트입니다.

컴프리헨션 안의 if 절은 필터이며, if/else가 아닙니다. 항목마다 한 번씩 실행되며, 조건이 참으로 평가되는 항목만 포함시킵니다. 조건에 따라 값을 다르게 변형하고 싶다면(조건에 따라 한 값을 다른 값으로 매핑하고 싶다면), 메인 표현식 안에 삼항 표현식을 쓰세요.

if의 위치가 필터링과 변형 중 무엇을 하는지를 결정하며, 이 둘을 헷갈리는 것이 결과 길이가 잘못되는 흔한 원인입니다. 맨 뒤에 오는 if는 필터입니다: [x for x in data if x > 0]은 항목을 걸러냅니다. 맨 앞에 오는 조건은 매핑입니다: [x if x > 0 else 0 for x in data]는 모든 항목을 유지하면서 실패한 항목만 다시 씁니다(여기서는 음수를 0으로 고정합니다). 둘을 한 컴프리헨션에서 결합할 수도 있고, [x * 2 for x in data if x > 0]처럼요, 뒤에 오는 if 절을 여러 개 쌓을 수도 있는데, 이들은 and로 연결됩니다.

python
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
evens = [n for n in numbers if n % 2 == 0]    # [2, 4, 6, 8]
odds = [n for n in numbers if n % 2 != 0]     # [1, 3, 5, 7]
python
scores = [87, 42, 96, 55, 71, 38]
passing = [s for s in scores if s >= 60]    # [87, 96, 71]
failing = [s for s in scores if s < 60]     # [42, 55, 38]
Juno조건으로 필터링하기 끝에 if condition을 추가하면 테스트를 통과한 항목만 남습니다: [x for x in data if x > 0]. 거짓으로 평가되는 것은 새 리스트에서 빠집니다. 이미 알고 있는 그 컴프리헨션에 문지기를 하나 세운 것뿐입니다.
Juno조건으로 필터링하기 뒤에 붙는 if는 필터로, 조건이 참인 항목만 남깁니다. 이는 표현식 안의 조건과는 다릅니다: [x if x > 0 else 0 for x in data]는 값을 버리는 대신 다시 씁니다. 어느 것을 원하는지 먼저 정하고 쓰세요.
Juno조건으로 필터링하기 끝에 오는 if는 필터라서 길이가 바뀌고, 앞에 오는 조건은 매핑이라 길이가 유지됩니다. 둘을 헷갈리면 크기가 잘못된 결과를 내보내게 됩니다. 뒤에 쌓인 if 절들은 and로 연결됩니다.

중첩 컴프리헨션

컴프리헨션을 중첩하면 리스트의 리스트를 하나의 리스트로 평탄화할 수 있습니다. 왼쪽에서 오른쪽으로 읽으세요: 각 행에 대해, 그 행의 각 항목에 대해, 항목을 포함시킵니다.

중첩 컴프리헨션은 왼쪽에서 오른쪽 순서로 실행됩니다. 첫 번째 for 절이 바깥 루프이고, 두 번째가 안쪽 루프입니다. 2차원 구조가 아니라 하나의 평탄한 결과를 만듭니다. 컴프리헨션을 한눈에 읽기 어렵다면, 루프를 명시적으로 작성하세요.

절의 순서는 동등한 중첩 루프와 같은 순서로 읽힙니다: 첫 번째 for가 바깥 루프이고, 두 번째가 안쪽 루프이며, 이후 절은 앞선 절이 바인딩한 이름을 쓸 수 있습니다. 이 방식이 값어치를 하는 경우는 평탄화이고, 오히려 손해가 되는 경우는 진짜 조합 격자(두 시퀀스의 모든 짝)를 만들 때인데, 이럴 때는 itertools.product가 이중 for보다 훨씬 명확하게 읽힙니다. 코드베이스를 온전하게 유지해주는 규칙: 컴프리헨션을 파악하는 데 1초 넘게 걸린다면, 명시적인 루프가 더 나은 문서 역할을 하므로 그걸 쓰세요.

python
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [item for row in matrix for item in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

왼쪽에서 오른쪽으로 읽으세요: matrix의 각 row에 대해, row의 각 item에 대해, item을 포함시킵니다.

중첩 컴프리헨션은 금방 헷갈릴 수 있습니다. 파악하는 데 잠깐보다 더 걸린다면, 루프를 명시적으로 작성하세요.

Juno중첩 컴프리헨션 컴프리헨션 하나에 for 절을 두 개 쌓으면 리스트의 리스트를 하나의 평탄한 리스트로 만들 수 있습니다: [item for row in matrix for item in row]. 왼쪽에서 오른쪽으로, 바깥 루프부터 읽으세요. 여러분이 직접 루프를 쓸 때와 정확히 같은 순서입니다. 눈이 걸린다면, 진짜 루프를 쓰라는 신호입니다.
Juno중첩 컴프리헨션for 절 두 개를 쌓으면 첫 번째가 바깥 루프, 두 번째가 안쪽 루프입니다: [item for row in matrix for item in row]는 2차원 리스트를 하나로 평탄화합니다. 결과는 평탄하지, 격자가 아닙니다. 한눈에 읽기 힘들어지면 명시적인 루프가 더 낫습니다.
Juno중첩 컴프리헨션 첫 번째 for가 바깥 루프이고, 이후 절은 앞선 절의 이름을 쓸 수 있습니다. 평탄화에는 훌륭하지만 모든 짝을 만드는 조합에는 부적합한데, 이때는 itertools.product가 더 깔끔하게 읽힙니다. 파악하는 데 1초 넘게 걸리면 루프가 더 나은 문서입니다.

딕셔너리 컴프리헨션

딕셔너리 컴프리헨션은 리스트 컴프리헨션과 같은 아이디어로 딕셔너리를 한 표현식으로 만듭니다: {key: value for item in iterable}. 리스트 컴프리헨션과 마찬가지로 if로 필터를 추가할 수 있습니다.

딕셔너리 컴프리헨션은 키-값 쌍을 만들어내는 어떤 이터러블로부터도 새 딕셔너리를 만들어냅니다. 문법은 {key_expr: val_expr for item in iterable if condition}입니다. 루프에서 나온 키가 중복되면 조용히 마지막 값이 사용됩니다. 딕셔너리 컴프리헨션의 소스로 가장 흔한 것은 기존 딕셔너리의 .items()입니다.

실제 코드에서 물리는 지점이 두 가지 있습니다. 첫째, 모든 키는 해시 가능(파이썬이 고정된 조회 번호로 환원할 수 있는 값, 즉 절대 변하지 않는 값이어야 하며, 문자열이나 숫자, 튜플은 해당되지만 리스트나 딕셔너리는 해당되지 않습니다)해야 합니다. 리스트를 반환하는 키 표현식은 TypeError를 일으킵니다. 둘째, 중복 키는 오류를 내지 않습니다. 루프가 같은 키를 두 번 만들어내면 나중 값이 조용히 앞선 값을 덮어씁니다. 이는 실제로 고유하지 않은 소스로 딕셔너리를 만들 때 데이터가 조용히 사라지는 원인이 됩니다. 이미 존재하는 두 딕셔너리를 그냥 합치고 싶을 때는, | 연산자(딕셔너리 병합 형태, Python 3.9 이상)가 컴프리헨션보다 의도를 더 명확하게 드러냅니다.

python
names = ["민준", "서연", "지호"]
scores = [87, 74, 92]

score_map = {name: score for name, score in zip(names, scores)}
# {"민준": 87, "서연": 74, "지호": 92}

필터를 함께 쓰면:

python
passing = {name: score for name, score in score_map.items() if score >= 80}
# {"민준": 87, "지호": 92}
python
words = ["apple", "banana", "cherry"]
word_lens = {word: len(word) for word in words}
# {"apple": 5, "banana": 6, "cherry": 6}
Juno딕셔너리 컴프리헨션{key: value for item in iterable}은 한 줄로 딕셔너리를 만듭니다. 리스트 컴프리헨션과 모양이 같고, 키와 값 사이에 콜론만 있을 뿐입니다. 이미 가진 딕셔너리 모양을 바꿀 때는 .items()와, 두 리스트를 하나의 매핑으로 엮을 때는 zip()과 짝지어 쓰세요. 끝에 if를 붙이면 원하는 쌍만 남길 수 있습니다.
Juno딕셔너리 컴프리헨션{key: value for item in iterable}은 쌍을 만들어내는 어떤 소스로부터든 딕셔너리를 만듭니다. 일상적으로 쓰는 두 가지 소스는 기존 딕셔너리의 .items()와 두 리스트에 대한 zip()입니다. 중복 키를 조심하세요. 마지막 것이 조용히 이깁니다.
Juno딕셔너리 컴프리헨션 키는 해시 가능해야 하고(리스트는 안 됨), 중복 키는 오류를 내지 않고 나중 값이 조용히 덮어써서 소스가 고유하지 않을 때 데이터를 먹어버립니다. 그러니 개수를 믿기 전에 고유성을 먼저 확인하세요. 두 딕셔너리를 합칠 때는 |가 컴프리헨션보다 더 직관적으로 말해줍니다.

집합 컴프리헨션

집합 컴프리헨션은 중괄호를 쓰고 콜론이 없는 형태로 집합을 한 표현식으로 만듭니다. 결과가 집합이므로 중복은 자동으로 제거됩니다.

집합 컴프리헨션은 {expression for item in iterable} 형태를 쓰며 set을 만들어냅니다. 자동으로 중복을 제거합니다. 순서가 중요하지 않은 변형 결과로부터 고유한 컬렉션이 필요할 때 사용하세요.

결과가 set이므로 리스트 버전에는 없는 두 가지 특징이 따라옵니다. 표현식에서 나온 중복 값은 자동으로 하나로 합쳐지는데, 이것이 바로 핵심입니다. 한 표현식으로 변형 결과를 중복 제거하는 가장 깔끔한 방법인 것입니다. 대가는 집합에 순서가 없다는 점이므로, 항목이 특정한 순서로 나온다고 기대할 수 없습니다. 구성원도 해시 가능해야 하는데, 이는 딕셔너리 키에 적용되는 것과 같은 규칙입니다: 파이썬이 고정된 조회 번호로 환원할 수 있는 값이어야 하며, 이는 리스트를 배제합니다.

python
words = ["apple", "banana", "cherry", "apple"]
unique = {w.lower() for w in words}    # {"apple", "banana", "cherry"}

고유한 값이 필요하고 순서는 상관없을 때 집합 컴프리헨션을 사용하세요.

Juno집합 컴프리헨션 중괄호를 쓰고 콜론이 없는 {expr for item in iterable}은 집합을 만들며, 집합은 공짜로 중복을 없애줍니다. 그러니 여러분의 목표가 "고유한 것만 달라"라면, 이걸로 한 줄이면 됩니다. 다만 어떤 순서로 나올지는 기대하지 마세요.
Juno집합 컴프리헨션{expr for item in iterable}set을 만들면서 진행하는 동안 중복을 제거합니다. 변형 결과로 고유한 컬렉션을 원하고 순서는 상관없을 때 쓰세요. 콜론이 없는 중괄호, 이게 딕셔너리 컴프리헨션과 구별해주는 유일한 차이입니다.
Juno집합 컴프리헨션 집합은 자동으로 중복을 없애주는데, 이것이 리스트 컴프리헨션 대신 집합을 고르는 이유 전부입니다. 대신 순서를 믿을 수 없고, 구성원은 해시 가능해야 하므로 안에 리스트를 넣을 수 없습니다. 가장 잘 맞는 용도: 한 표현식으로 변형 결과를 중복 제거하기.

제너레이터 표현식

제너레이터는 대괄호 대신 소괄호를 쓴다는 점에서 리스트 컴프리헨션과 비슷하게 생겼습니다. 핵심 차이는 이렇습니다: 리스트 컴프리헨션은 전체 리스트를 한 번에 메모리에 만듭니다. 제너레이터는 필요할 때만 값을 하나씩 만들어냅니다. 큰 시퀀스에서는 이 방식이 메모리를 훨씬 적게 씁니다.

제너레이터 표현식은 컬렉션이 아니라 이터레이터를 만들어냅니다. 값을 지연 계산합니다: 다음 값은 요청받을 때만 만들어집니다. 결과를 sum(), max(), any() 같은 함수가 바로 소비하는 경우에 가장 유용한데, 전체 리스트를 먼저 만들어봐야 아무 의미가 없기 때문입니다.

제너레이터는 값을 지연 생성(한 번에 하나씩, 다음 것을 누군가 요청할 때만 만드는 방식)하므로, 입력이 얼마나 크든 메모리 사용량이 일정하게 유지됩니다. 현재 값 하나 이상을 절대 붙잡지 않는데, 리스트 컴프리헨션은 모든 원소를 한꺼번에 붙잡습니다. 그래서 크거나 스트리밍되는 소스를 곧바로 sum(), max(), any()에 흘려넣을 때 제너레이터가 올바른 선택입니다. 기억해야 할 실패 유형은, 제너레이터가 한 번만 쓸 수 있다는 점입니다. 무언가가 끝까지 순회하면 제너레이터는 소진되어 더는 아무것도 내놓지 않으므로, 같은 제너레이터에 대한 두 번째 루프는 오류 없이 그냥 0번 실행됩니다. 데이터를 두 번 훑어야 한다면, 리스트를 한 번 만들어서 그것을 순회하세요.

python
squares_gen = (n ** 2 for n in range(1000000))
python
total = sum(n ** 2 for n in range(1000000))   # sum()이 제너레이터를 소비함

sum(), max(), min(), any() 같은 함수에 제너레이터를 바로 넘길 때는 괄호를 하나 더 쓰지 않아도 됩니다:

python
total = sum(n ** 2 for n in range(1000))   # 괄호 두 겹이 아니라 한 겹

일상적인 코드 대부분에서는 리스트 컴프리헨션으로 충분합니다. 큰 데이터셋을 처리하거나, 모든 것을 메모리에 올려두는 것이 낭비인 스트리밍 데이터를 다룰 때 제너레이터를 사용하세요.

Juno제너레이터 표현식 제너레이터는 대괄호 대신 소괄호를 쓴다는 점에서 리스트 컴프리헨션처럼 보이지만, 리스트 전체를 미리 만드는 대신 값을 하나씩 만들어냅니다. 거대한 시퀀스라면 메모리를 상당히 아껴줍니다. 좋은 예: sum()이나 max()에 바로 넣고 리스트를 만드는 과정을 통째로 건너뛰기.
Juno제너레이터 표현식 제너레이터 표현식은 지연 이터레이터를 반환합니다: 요청받을 때만 다음 값을 계산하므로, 전체 리스트가 메모리에 만들어지지 않습니다. 결과를 sum(), max(), any()에 바로 흘려넣을 때 가장 좋습니다. 한 번만 쓸 수 있다는 점을 기억하세요. 소비되면 비어 있습니다.
Juno제너레이터 표현식 입력이 얼마나 크든 지연적이고 메모리가 일정하게 유지되므로, sum()이나 any()에 흘려넣는 크거나 스트리밍되는 소스에 맞는 도구입니다. 함정은 한 번만 쓸 수 있다는 것이고, 소진된 제너레이터에 대한 두 번째 루프는 오류 없이 0번 실행됩니다. 두 번 훑어야 한다면? 리스트를 한 번 만드세요.

zip()

zip()은 둘 이상의 시퀀스에서 항목을 짝지어서 나란히 순회할 수 있게 해줍니다. 가장 짧은 시퀀스에서 멈춥니다. 두 리스트가 서로 대응할 때 인덱스를 관리하지 않고 처리하는 깔끔한 방법입니다.

zip()은 튜플의 지연 이터레이터를 반환하며, 입력 이터러블들을 나란히 소비합니다. 가장 짧은 입력에서 멈추며, 더 긴 시퀀스는 조용히 잘려나갑니다. 시퀀스 길이가 다를 수 있는 경우, itertools.zip_longest()가 짧은 쪽을 지정한 값으로 채워줍니다.

zip()은 입력들을 나란히 순회하다가 가장 짧은 것이 끝나는 순간 멈춥니다. 그 잘림 현상이 설계할 때 조심해야 할 함정입니다: 1000개짜리 레코드 리스트와 999개짜리 리스트를 짝지으면 오류도 경고도 없이 마지막 레코드를 조용히 잃어버립니다. 길이가 반드시 같아야 하는 경우에는, zip(seq_a, seq_b, strict=True)(Python 3.10 이상)가 데이터를 버리는 대신 길이가 다르면 예외를 일으킵니다. 길이가 달라도 되는 경우에는 itertools.zip_longest가 빈 자리를 채워줍니다. 알아두면 좋은 또 다른 트릭: zip(*rows)는 전치를 수행해서, 행의 리스트를 열의 리스트로 바꿔줍니다. *가 바깥 리스트를 개별 인자로 풀어내기 때문입니다.

python
names = ["민준", "서연", "지호"]
scores = [87, 74, 92]

for name, score in zip(names, scores):
    print(f"{name}: {score}")
# 민준: 87
# 서연: 74
# 지호: 92

zip()은 가장 짧은 시퀀스에서 멈춥니다. 시퀀스 길이가 다를 수 있다면, 채움 값을 지정해서 itertools.zip_longest()를 사용하세요.

짝지어진 리스트를 다시 두 개의 별도 리스트로 되돌리려면 zip(*pairs)를 사용하세요:

python
pairs = [("민준", 87), ("서연", 74), ("지호", 92)]
names, scores = zip(*pairs)
# names = ("민준", "서연", "지호")
# scores = (87, 74, 92)

*pairs는 리스트를 개별 인자로 풀어내므로, zip(*pairs)zip(("민준", 87), ("서연", 74), ("지호", 92))가 됩니다. * 연산자는 함수 장에서 다룹니다.

zip()은 인덱스를 직접 관리하지 않고 여러 시퀀스를 병렬로 순회하는 깔끔한 방법이기도 합니다:

python
before = [10, 20, 30]
after = [15, 18, 35]

for b, a in zip(before, after):
    change = a - b
    print(f"{b} -> {a} ({'+' if change >= 0 else ''}{change})")
Junozip()zip()은 둘 이상의 시퀀스를 짝지어서 인덱스 곡예 없이 함께 순회할 수 있게 해줍니다. 가장 짧은 것에서 멈추므로, 길이가 맞지 않으면 나머지가 조용히 사라집니다. 그리고 zip(*pairs)는 이걸 거꾸로 돌려서, 튜플의 리스트를 다시 별도의 리스트들로 나눠줍니다.
Junozip()zip()은 튜플의 지연 이터레이터를 반환하며 입력들을 병렬로 밟아 나갑니다. 가장 짧은 것에서 멈추므로, 더 긴 시퀀스는 소리 없이 잘려나갑니다. itertools.zip_longest()는 길이가 정말로 달라도 될 때 빈 자리를 채워주고, zip(*pairs)는 다시 풀어줍니다.
Junozip() 지연적이고 병렬적이며 가장 짧은 입력에서 멈추는데, 이는 길이가 맞아야 하는 상황에서 조용히 데이터를 버립니다. strict=True를 넘기면 불일치 시 오류를 일으키게 만들 수 있습니다. zip(*rows)는 전치 연산으로, 한 번에 행을 열로 바꿔줍니다.

map()과 filter()

map()filter()는 컴프리헨션이 하는 일을 해내는 더 오래된 함수형 스타일 도구입니다. 오래된 코드에서 자주 마주치므로, 무슨 의미인지 알아두면 좋습니다. 새 코드에서는 컴프리헨션을 우선시하세요. 대부분의 파이썬 개발자에게 더 읽기 쉽습니다.

map(func, iterable)은 각 항목에 func를 적용하는 지연 이터레이터를 반환합니다. filter(func, iterable)func가 참으로 평가되는 항목들의 지연 이터레이터를 반환합니다. 둘 다 컴프리헨션보다 먼저 생긴 도구입니다. 새 코드에서는 컴프리헨션을 우선시하고, 필요한 일을 이미 하는 이름 있는 함수가 있을 때 map()을 쓰세요.

둘 다 리스트가 아니라 지연 이터레이터를 반환하므로, 지금 값이 필요하다면 list()로 감싸야 합니다. 이미 알고 있는 컴프리헨션과 일대일로 대응됩니다: map(f, it)(f(x) for x in it)이고, filter(pred, it)(x for x in it if pred(x))입니다. 그 대응 관계가 선택 기준이 됩니다. 인라인 lambda를 쓴다면 컴프리헨션이 더 잘 읽히고, 요즘은 이게 기본 선택입니다. 이미 필요한 일을 해주는 이름 있는 함수가 있다면, list(map(int, strings))는 "strings에 int를 map한다"로 읽혀서 더 깔끔한 선택입니다. 결국 진짜 결정 기준은 손에 이미 함수가 있느냐뿐입니다.

python
numbers = [1, 2, 3, 4, 5]

list(map(lambda x: x ** 2, numbers))         # [1, 4, 9, 16, 25]
list(filter(lambda x: x % 2 == 0, numbers))  # [2, 4]

컴프리헨션을 우선시하세요. 대부분의 파이썬 개발자에게 더 읽기 쉽습니다. 이미 존재하는 이름 있는 함수가 있을 때는 map()을 사용하세요:

python
strings = ["1", "2", "3"]
numbers = list(map(int, strings))   # [1, 2, 3] (여기서는 컴프리헨션보다 깔끔함)
Junomap()과 filter()map(func, iterable)은 모든 항목에 함수를 실행하고, filter(func, iterable)은 함수가 참으로 돌아오는 항목만 남깁니다. 컴프리헨션이 하는 일을 하는 더 오래된 방식이라, 다른 사람 코드에서 마주치게 될 겁니다. 여러분 코드라면, 대부분의 사람에게 컴프리헨션이 더 명확하게 읽힙니다.
Junomap()과 filter()map()은 각 항목을 변형하고, filter()는 참인 것만 남기며, 둘 다 지연 평가되므로 결과를 보려면 list()로 감싸세요. 새 코드에서는 컴프리헨션이 기본입니다. map()이 이기는 유일한 자리는 이미 있는 이름 있는 함수를 쓸 때입니다: map(int, strings)가 컴프리헨션보다 깔끔하게 읽힙니다.
Junomap()과 filter()map(f, it)(f(x) for x in it)이고 filter(pred, it)(x for x in it if pred(x))이며, 둘 다 지연적입니다. 그러니 선택은 순전히 가독성 문제입니다: 인라인 람다라면 컴프리헨션이 이기고, 이미 이름 붙은 함수가 있다면 map(int, strings)가 이깁니다.

실전 예제

플레이어 목록을 통과 점수로 필터링하고, sorted와 람다로 점수 순위를 매긴 뒤, 순번을 붙여 출력합니다:

python
players = [
    {"name": "민준", "score": 87},
    {"name": "서연", "score": 42},
    {"name": "지호", "score": 96},
    {"name": "다은", "score": 55},
]

passing = [p for p in players if p["score"] >= 60]
ranked = sorted(passing, key=lambda p: p["score"], reverse=True)
score_map = {p["name"]: p["score"] for p in ranked}

for i, (name, score) in enumerate(score_map.items(), start=1):
    print(f"{i}. {name}: {score}")

사용자 목록에서 활성 상태인 관리자를 필터링하고, id-이름 조회용 딕셔너리를 만들고, 각각 한 번씩 순회하며 정렬된 이름 목록을 모읍니다:

python
raw_users = [
    {"id": 1, "name": "민준", "role": "admin", "active": True},
    {"id": 2, "name": "서연", "role": "user", "active": False},
    {"id": 3, "name": "지호", "role": "admin", "active": True},
    {"id": 4, "name": "다은", "role": "user", "active": True},
]

active_admins = [u for u in raw_users if u["active"] and u["role"] == "admin"]
id_map = {u["id"]: u["name"] for u in raw_users}
names = sorted(u["name"] for u in raw_users if u["active"])

print(f"Active admins: {[u['name'] for u in active_admins]}")
print(f"All active: {names}")

zip으로 특성 이름과 중요도 점수를 짝짓고, 딕셔너리 컴프리헨션을 만들고, 람다로 정렬한 뒤, 두 번째 컴프리헨션으로 값을 정규화합니다:

python
feature_names = ["age", "income", "score", "tenure"]
importances = [0.12, 0.34, 0.28, 0.26]

feat_dict = {f: i for f, i in zip(feature_names, importances)}
top_feats = sorted(feat_dict.items(), key=lambda x: x[1], reverse=True)[:2]

print("Top 2 features:")
for name, score in top_feats:
    print(f"  {name}: {score:.2f}")

# 합이 1.0이 되도록 정규화 (여기서는 이미 합이 1이지만, 패턴을 보여주기 위함)
total = sum(feat_dict.values())
normalised = {k: round(v / total, 4) for k, v in feat_dict.items()}
print(f"Normalised: {normalised}")

zip은 중간 튜플을 만들지 않고 두 리스트를 짝짓습니다. 딕셔너리 컴프리헨션은 매핑을 한 표현식으로 만듭니다. 정렬용 람다는 이름 있는 key 함수를 따로 만들지 않게 해줍니다. 정규화 컴프리헨션은 원래 딕셔너리를 변경하지 않으면서 값을 변형합니다.