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

람다와 컴프리헨션

이 세 가지 기능은 공통점이 있습니다. 평소라면 여러 줄이 필요한 아이디어를 읽기 쉬운 단일 표현식으로 표현할 수 있게 해줍니다. 잘 사용하면 코드가 더 짧고 명확해집니다. 잘못 사용하면 읽을 수 없게 됩니다. 이 장에서는 각각을 언제 사용하고 언제 멈춰야 하는지 다룹니다.

람다, 컴프리헨션, zip은 일반적인 패턴을 표현식으로 압축하는 세 가지 도구입니다. 필수는 아니지만, Python 코드 전반에 등장하므로 알아보고 능숙하게 작성할 가치가 있습니다. 핵심 원칙은 단지 더 짧기 때문이 아니라 의도를 더 명확하게 만들 때 사용하라는 것입니다.

람다 표현식은 런타임에 익명 함수 객체를 생성합니다. 컴프리헨션은 외부 프레임에서 for 루프 없이 컬렉션을 만드는 최적화된 바이트코드로 컴파일됩니다. 제너레이터는 지연(lazy) 평가됩니다. 전체 시퀀스를 구체화하지 않고 필요할 때 값을 산출합니다. zip은 입력 이터러블을 지연 소비하면서 튜플의 이터레이터를 반환합니다. 세 가지 모두 명령형 루프가 아니라 표현식으로 변환을 나타낸다는 공통 주제를 공유합니다.

람다 함수

람다는 이름이 없는, 표현식 하나로 된 함수입니다. lambda 키워드로 만듭니다. 진짜 유용한 점은 이름 있는 함수를 먼저 정의할 필요 없이, 필요한 곳에 인라인으로 작성할 수 있다는 것입니다. 그래서 sorted()와 함께 쓰면 유용합니다.

람다는 단일 표현식으로 된 익명 함수입니다. 여러 인수를 받을 수 있지만 본문은 문(statement)이 아닌 단일 표현식이어야 합니다. 주된 용도는 인라인 key= 또는 콜백 인수로 사용하는 것이며, 전체 def를 정의하면 불필요한 간접 참조가 생기는 경우에 적합합니다. 더 복잡한 경우에는 def를 사용하세요.

lambda args: expression은 코드 객체로 컴파일되어 함수 객체를 만들며, 이름이 없다는 점(트레이스백에 <lambda>로 표시됨)과 문을 포함할 수 없고 docstring이나 어노테이션을 지원하지 않는다는 점을 제외하면 def와 동일합니다. 람다는 클로저에 참여합니다. 자유 변수는 둘러싸는 스코프에서 캡처됩니다. 흔한 함정은 루프 안의 lambda i: ii를 값이 아니라 참조로 캡처한다는 점입니다. 생성 시점의 값을 바인딩하려면 lambda 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로 전환하라는 신호입니다.

리스트 컴프리헨션

Python에서 가장 일반적인 변환입니다. 시퀀스를 받아서 각 항목에 무언가를 하고 새 리스트를 얻습니다. 리스트 컴프리헨션은 이를 읽기 쉬운 한 줄로 처리합니다: [expression for item in iterable]. if로 필터도 추가할 수 있습니다.

리스트 컴프리헨션은 루프로 빌드하는 패턴을 간결하게 대체합니다. 최적화된 바이트코드로 컴파일되며, 일반적으로 .append()를 사용한 동등한 for 루프보다 빠릅니다. 구조는 [expression for item in iterable if condition]이며 if 절은 선택 사항입니다.

리스트 컴프리헨션은 전용 바이트코드의 LIST_APPEND 루프로 컴파일되며, Python 수준 루프에서 반복되는 list.append() 호출보다 빠릅니다. Python 3에서는 새로운 스코프를 생성하므로(Python 2와 달리) 루프 변수가 외부로 누출되지 않습니다. 중첩 컴프리헨션은 왼쪽에서 오른쪽으로, 위에서 아래로 실행됩니다. [expr for x in a for y in b]x가 바깥 루프인 중첩 for 루프와 동등합니다.

긴 방법:

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]    # 각 항목 포맷팅

조건으로 필터링

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

컴프리헨션의 if 절은 if/else가 아니라 필터입니다. 각 항목에 대해 한 번 실행되며, 조건이 참인 항목만 포함합니다. 조건부 변환(조건에 따라 한 값을 다른 값으로 매핑)을 원하면 주 표현식 안에 삼항 표현식을 사용하세요.

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]

중첩 컴프리헨션

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

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

중첩 컴프리헨션은 첫 번째 for를 가장 바깥 루프로 하여 중첩 루프로 실행됩니다. 각 루프 변수의 스코프는 이후 절에서 사용 가능합니다. 곱집합(Cartesian product)에는 itertools.product가 더 명확한 경우가 많습니다. 핵심 가독성 규칙은 컴프리헨션을 파싱하는 데 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을 포함합니다.

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

딕셔너리 컴프리헨션

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

딕셔너리 컴프리헨션은 키-값 쌍을 만드는 임의의 이터러블로부터 새 딕셔너리를 생성합니다. 문법은 {key_expr: val_expr for item in iterable if condition}입니다. 루프에서 중복 키가 발생하면 조용히 마지막 값이 사용됩니다. 기존 딕셔너리의 .items()가 딕셔너리 컴프리헨션의 가장 흔한 소스 이터러블입니다.

딕셔너리 컴프리헨션은 리스트 컴프리헨션의 LIST_APPEND와 유사한 전용 MAP_ADD 바이트코드로 컴파일됩니다. Python 3에서는 새 스코프를 생성합니다. 키 표현식은 해시 가능한 값을 만들어야 하며, 키 표현식이 중복을 생성하면 나중 값이 조용히 이깁니다. 순서 있는 병합 의미에는 컴프리헨션보다 | 연산자(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}

셋 컴프리헨션

셋 컴프리헨션은 콜론 없이 중괄호를 사용하여 한 표현식에서 집합을 만듭니다. 결과가 집합이므로 중복은 자동으로 제거됩니다.

셋 컴프리헨션은 {expression for item in iterable}을 사용하여 set을 생성합니다. 자동으로 중복을 제거합니다. 순서가 중요하지 않고 변환을 통해 유일한 컬렉션이 필요할 때 사용하세요.

셋 컴프리헨션은 SET_ADD 바이트코드로 컴파일됩니다. 결과는 순서 없는 집합이며, 표현식에서 중복된 값은 조용히 병합됩니다. 셋 컴프리헨션은 리스트나 딕셔너리 컴프리헨션보다 덜 흔하지만, 한 표현식으로 중복이 제거된 변환을 생성하는 깔끔한 방법입니다.

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

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

제너레이터 표현식

제너레이터는 대괄호 대신 괄호를 사용하는 리스트 컴프리헨션처럼 보입니다. 핵심 차이는 다음과 같습니다. 리스트 컴프리헨션은 전체 리스트를 한 번에 메모리에 만듭니다. 제너레이터는 필요할 때만 한 번에 하나씩 값을 생성합니다. 큰 시퀀스의 경우 메모리를 훨씬 적게 사용합니다.

제너레이터 표현식은 컬렉션이 아닌 이터레이터를 생성합니다. 값을 지연 계산합니다. 다음 값은 요청될 때에만 생성됩니다. 이는 결과가 sum(), max(), any() 같은 함수로 즉시 소비될 때 가장 유용합니다. 전체 리스트를 먼저 만들 이유가 없기 때문입니다.

제너레이터 표현식은 코드 객체로 컴파일되어 제너레이터 객체를 반환합니다. 값은 __next__를 통해 지연 생성되므로 입력 크기에 관계없이 메모리 사용량은 O(1)입니다. 이터레이터 프로토콜에 참여하며 체이닝할 수 있습니다. 이터러블을 받는 함수에 직접 전달될 때는 바깥 괄호를 생략할 수 있습니다. 제너레이터는 소진된 후에는 재사용할 수 없습니다. 여러 번 반복해야 한다면 리스트로 구체화하세요.

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))   # 괄호 두 쌍이 아니라 한 쌍

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

zip()

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

zip()은 튜플의 지연 이터레이터를 반환하며, 입력 이터러블을 동기적으로 소비합니다. 가장 짧은 입력에서 멈춥니다. 더 긴 시퀀스는 조용히 잘립니다. 길이가 다를 수 있는 시퀀스에는 itertools.zip_longest()가 지정된 값으로 짧은 쪽을 채워줍니다.

zip()zip 객체를 반환합니다. 이는 각 입력 이터레이터에서 next()를 동시에 호출하는 지연 이터레이터입니다. 임의의 이터레이터가 StopIteration을 발생시키면 멈춥니다. 모든 입력은 지연 소비됩니다. zip() 자체는 입력 크기에 관계없이 O(1) 메모리를 할당합니다. zip(*iterable)은 표준 전치 연산입니다. *는 외부 이터러블을 별도 인수로 언패킹합니다.

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})")

map()과 filter()

map()filter()는 컴프리헨션이 하는 일을 수행하는 오래된 함수형 스타일 도구입니다. 오래된 코드에서 볼 수 있으므로 의미를 알아두는 것이 좋습니다. 새 코드에서는 컴프리헨션을 선호하세요. 대부분의 Python 개발자에게 더 읽기 쉽습니다.

map(func, iterable)func을 각 항목에 적용하는 지연 이터레이터를 반환합니다. filter(func, iterable)func이 참인 항목의 지연 이터레이터를 반환합니다. 둘 다 컴프리헨션보다 먼저 나왔습니다. 새 코드에서는 컴프리헨션을 선호하세요. 원하는 동작을 하는 이름 있는 함수가 이미 있을 때는 map()을 사용하세요.

map()filter()는 Python 3에서 (리스트가 아닌) 지연 이터레이터를 반환합니다. map(f, it)(f(x) for x in it)과 동등합니다. filter(pred, it)(x for x in it if pred(x))와 동등합니다. 이름 있는 함수에 대해서는 list(map(int, strings))가 "strings에 int를 매핑"으로 읽혀서 관용적입니다. 동등한 컴프리헨션 [int(s) for s in strings]도 똑같이 유효합니다.

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]

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

python
strings = ["1", "2", "3"]
numbers = list(map(int, strings))   # [1, 2, 3] (여기서는 컴프리헨션보다 깔끔함)

실전에서

플레이어 목록을 합격 점수로 필터링하고, 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 함수를 피합니다. 정규화 컴프리헨션은 원본 딕셔너리를 변형하지 않고 값을 변환합니다.