람다와 컴프리헨션
이 세 가지 기능에는 공통점이 있습니다. 원래는 여러 줄이 필요했을 아이디어를, 읽기 쉬운 한 줄의 표현식으로 표현할 수 있게 해줍니다. 잘 쓰면 코드가 더 짧고 명확해지지만, 잘못 쓰면 읽기 힘든 코드가 됩니다. 이 장에서는 각각을 언제 써야 하고, 언제 멈춰야 하는지를 다룹니다.
람다 함수
람다는 이름이 없고 표현식 하나로만 이루어진 함수입니다. lambda 키워드로 만듭니다. 람다의 진짜 쓸모는, 이름 있는 함수를 따로 정의하지 않고 필요한 자리에 바로 인라인으로 쓸 수 있다는 점입니다. 이 특성 덕분에 sorted()와 함께 쓸 때 유용합니다.
double = lambda x: x * 2
double(5) # 10다음과 동일합니다:
def double(x):
return x * 2대부분의 경우 def를 쓰세요. 람다에는 진짜 장점이 하나 있습니다: 이름을 붙이지 않고 필요한 자리에 바로 인라인으로 쓸 수 있다는 점입니다. 이 특성 덕분에 sorted(), map(), filter()와 함께 쓸 때 유용합니다:
players = [("민준", 87), ("서연", 74), ("지호", 92)]
sorted(players, key=lambda p: p[1]) # 점수 기준 오름차순 정렬
sorted(players, key=lambda p: p[1], reverse=True) # 점수 기준 내림차순 정렬람다가 없다면, key= 인자를 위해서만 이름 있는 함수를 정의해야 합니다. 람다는 의도를 그 자리에 국한시켜 눈에 보이게 유지해줍니다.
람다는 여러 인자를 받을 수 있습니다:
add = lambda a, b: a + b
add(3, 4) # 7람다를 써야 할 때: 한 곳에서만 쓰이는 작은 표현식일 때만 쓰세요. 점점 커지거나 재사용이 필요하다면 제대로 된 def를 작성하세요. 여러 연산자에 걸쳐 있거나 조건문이 필요한 람다는 대개 def로 바꿔야 한다는 신호입니다.
lambda 키워드로 쓰는 한 줄짜리 작은 함수입니다. 존재 이유는 딱 하나, sorted()에 쓸 key=로 인라인으로 들어가서, 한 가지 일만 하는 함수를 따로 정의하지 않게 해주는 것입니다. 깔끔한 표현식 한 줄을 넘어서는 순간 저는 진짜 def를 씁니다. 그게 마음이 편합니다. 리스트 컴프리헨션
파이썬에서 가장 흔한 변형 작업은 이렇습니다: 시퀀스를 가져와 각 항목에 어떤 작업을 하고, 새로운 리스트를 돌려받는 것입니다. 리스트 컴프리헨션은 이 작업을 읽기 쉬운 한 줄로 해냅니다: [expression for item in iterable]. if로 필터를 추가할 수도 있습니다.
긴 방법:
numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
squares.append(n ** 2)리스트 컴프리헨션:
squares = [n ** 2 for n in numbers]구조는 항상 같습니다: [expression for item in iterable].
scores = [87, 42, 96, 55, 71]
scaled = [s * 1.1 for s in scores] # 10% 보너스 적용
as_grades = [f"{s}/100" for s in scores] # 각 항목을 포맷[expression for item in iterable]은 시퀀스를 가져와 각 항목에 한 가지 일을 하고, 새로운 리스트를 돌려줍니다. 왼쪽에서 오른쪽으로 읽으면 하는 일이 그대로 보입니다. 저는 이 기능을 처음 봤을 때, 다른 언어를 번역하는 게 아니라 진짜 파이썬을 쓰고 있다는 느낌을 처음 받았습니다. 조건으로 필터링하기
if 절을 추가하면 테스트를 통과한 항목만 포함시킬 수 있습니다. 결과는 조건이 True인 항목만 담긴 새 리스트입니다.
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]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]if condition을 추가하면 테스트를 통과한 항목만 남습니다: [x for x in data if x > 0]. 거짓으로 평가되는 것은 새 리스트에서 빠집니다. 이미 알고 있는 그 컴프리헨션에 문지기를 하나 세운 것뿐입니다. 중첩 컴프리헨션
컴프리헨션을 중첩하면 리스트의 리스트를 하나의 리스트로 평탄화할 수 있습니다. 왼쪽에서 오른쪽으로 읽으세요: 각 행에 대해, 그 행의 각 항목에 대해, 항목을 포함시킵니다.
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을 포함시킵니다.
중첩 컴프리헨션은 금방 헷갈릴 수 있습니다. 파악하는 데 잠깐보다 더 걸린다면, 루프를 명시적으로 작성하세요.
for 절을 두 개 쌓으면 리스트의 리스트를 하나의 평탄한 리스트로 만들 수 있습니다: [item for row in matrix for item in row]. 왼쪽에서 오른쪽으로, 바깥 루프부터 읽으세요. 여러분이 직접 루프를 쓸 때와 정확히 같은 순서입니다. 눈이 걸린다면, 진짜 루프를 쓰라는 신호입니다. 딕셔너리 컴프리헨션
딕셔너리 컴프리헨션은 리스트 컴프리헨션과 같은 아이디어로 딕셔너리를 한 표현식으로 만듭니다: {key: value for item in iterable}. 리스트 컴프리헨션과 마찬가지로 if로 필터를 추가할 수 있습니다.
names = ["민준", "서연", "지호"]
scores = [87, 74, 92]
score_map = {name: score for name, score in zip(names, scores)}
# {"민준": 87, "서연": 74, "지호": 92}필터를 함께 쓰면:
passing = {name: score for name, score in score_map.items() if score >= 80}
# {"민준": 87, "지호": 92}words = ["apple", "banana", "cherry"]
word_lens = {word: len(word) for word in words}
# {"apple": 5, "banana": 6, "cherry": 6}{key: value for item in iterable}은 한 줄로 딕셔너리를 만듭니다. 리스트 컴프리헨션과 모양이 같고, 키와 값 사이에 콜론만 있을 뿐입니다. 이미 가진 딕셔너리 모양을 바꿀 때는 .items()와, 두 리스트를 하나의 매핑으로 엮을 때는 zip()과 짝지어 쓰세요. 끝에 if를 붙이면 원하는 쌍만 남길 수 있습니다. 집합 컴프리헨션
집합 컴프리헨션은 중괄호를 쓰고 콜론이 없는 형태로 집합을 한 표현식으로 만듭니다. 결과가 집합이므로 중복은 자동으로 제거됩니다.
words = ["apple", "banana", "cherry", "apple"]
unique = {w.lower() for w in words} # {"apple", "banana", "cherry"}고유한 값이 필요하고 순서는 상관없을 때 집합 컴프리헨션을 사용하세요.
{expr for item in iterable}은 집합을 만들며, 집합은 공짜로 중복을 없애줍니다. 그러니 여러분의 목표가 "고유한 것만 달라"라면, 이걸로 한 줄이면 됩니다. 다만 어떤 순서로 나올지는 기대하지 마세요. 제너레이터 표현식
제너레이터는 대괄호 대신 소괄호를 쓴다는 점에서 리스트 컴프리헨션과 비슷하게 생겼습니다. 핵심 차이는 이렇습니다: 리스트 컴프리헨션은 전체 리스트를 한 번에 메모리에 만듭니다. 제너레이터는 필요할 때만 값을 하나씩 만들어냅니다. 큰 시퀀스에서는 이 방식이 메모리를 훨씬 적게 씁니다.
squares_gen = (n ** 2 for n in range(1000000))total = sum(n ** 2 for n in range(1000000)) # sum()이 제너레이터를 소비함sum(), max(), min(), any() 같은 함수에 제너레이터를 바로 넘길 때는 괄호를 하나 더 쓰지 않아도 됩니다:
total = sum(n ** 2 for n in range(1000)) # 괄호 두 겹이 아니라 한 겹일상적인 코드 대부분에서는 리스트 컴프리헨션으로 충분합니다. 큰 데이터셋을 처리하거나, 모든 것을 메모리에 올려두는 것이 낭비인 스트리밍 데이터를 다룰 때 제너레이터를 사용하세요.
sum()이나 max()에 바로 넣고 리스트를 만드는 과정을 통째로 건너뛰기. zip()
zip()은 둘 이상의 시퀀스에서 항목을 짝지어서 나란히 순회할 수 있게 해줍니다. 가장 짧은 시퀀스에서 멈춥니다. 두 리스트가 서로 대응할 때 인덱스를 관리하지 않고 처리하는 깔끔한 방법입니다.
names = ["민준", "서연", "지호"]
scores = [87, 74, 92]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# 민준: 87
# 서연: 74
# 지호: 92zip()은 가장 짧은 시퀀스에서 멈춥니다. 시퀀스 길이가 다를 수 있다면, 채움 값을 지정해서 itertools.zip_longest()를 사용하세요.
짝지어진 리스트를 다시 두 개의 별도 리스트로 되돌리려면 zip(*pairs)를 사용하세요:
pairs = [("민준", 87), ("서연", 74), ("지호", 92)]
names, scores = zip(*pairs)
# names = ("민준", "서연", "지호")
# scores = (87, 74, 92)*pairs는 리스트를 개별 인자로 풀어내므로, zip(*pairs)는 zip(("민준", 87), ("서연", 74), ("지호", 92))가 됩니다. * 연산자는 함수 장에서 다룹니다.
zip()은 인덱스를 직접 관리하지 않고 여러 시퀀스를 병렬로 순회하는 깔끔한 방법이기도 합니다:
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})")zip()은 둘 이상의 시퀀스를 짝지어서 인덱스 곡예 없이 함께 순회할 수 있게 해줍니다. 가장 짧은 것에서 멈추므로, 길이가 맞지 않으면 나머지가 조용히 사라집니다. 그리고 zip(*pairs)는 이걸 거꾸로 돌려서, 튜플의 리스트를 다시 별도의 리스트들로 나눠줍니다. map()과 filter()
map()과 filter()는 컴프리헨션이 하는 일을 해내는 더 오래된 함수형 스타일 도구입니다. 오래된 코드에서 자주 마주치므로, 무슨 의미인지 알아두면 좋습니다. 새 코드에서는 컴프리헨션을 우선시하세요. 대부분의 파이썬 개발자에게 더 읽기 쉽습니다.
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()을 사용하세요:
strings = ["1", "2", "3"]
numbers = list(map(int, strings)) # [1, 2, 3] (여기서는 컴프리헨션보다 깔끔함)map(func, iterable)은 모든 항목에 함수를 실행하고, filter(func, iterable)은 함수가 참으로 돌아오는 항목만 남깁니다. 컴프리헨션이 하는 일을 하는 더 오래된 방식이라, 다른 사람 코드에서 마주치게 될 겁니다. 여러분 코드라면, 대부분의 사람에게 컴프리헨션이 더 명확하게 읽힙니다. 실전 예제
플레이어 목록을 통과 점수로 필터링하고, sorted와 람다로 점수 순위를 매긴 뒤, 순번을 붙여 출력합니다:
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}")
