리스트 컴프리헨션은 짧게 쓰기 위한 장식이 아니라, 새 리스트 결과를 만들고 싶을 때 쓰는 표현입니다.
변환, 필터, 조건식, generator expression과의 경계가 함께 잡히면 for 루프보다 더 빨리 읽히는 경우가 많습니다.
숏컷 코드
numbers = [1, 2, 3, 4, 5]
# 값 변환
squares = [n * n for n in numbers]
# 일부만 남김
passed = [n for n in numbers if n >= 3]
# 값 가공 + 빈 값 제거
labels = [name.strip().title() for name in raw_names if name.strip()]
# 결과 값을 둘 중 하나로 나눔
status = ["pass" if score >= 80 else "retry" for score in scores]
# 리스트 대신 지연 평가
square_iter = (n * n for n in numbers)문법
기본형은 변환, 필터, 조건식, 중첩, generator expression 다섯 가지로 보면 됩니다.
[expr for item in items]
[item for item in items if cond]
[a if cond else b for item in items]
[expr for left in xs for right in ys]
(expr for item in items)앞쪽 식은 결과를 무엇으로 만들지 정합니다.
expr 자리에 들어가는 부분이 최종 리스트 원소가 됩니다.
뒤쪽 if는 필터입니다.
조건을 만족하는 항목만 결과에 남깁니다.
앞쪽의 a if cond else b는 결과 값을 나누는 조건식입니다.
즉 "남길지 말지"가 아니라 "무슨 값을 넣을지"를 고르는 자리입니다.
괄호로 쓰면 리스트가 아니라 generator expression입니다.
모양은 비슷하지만 바로 리스트를 만드는 것이 아니라, 필요할 때 하나씩 꺼내는 흐름이 됩니다.
for가 두 번 들어가면 중첩 순회 결과를 한 줄에서 만든다는 뜻입니다.
pairs = [(x, y) for x in xs for y in ys]대표 패턴
값을 바꿔 새 리스트를 만들 때 가장 기본적으로 씁니다.
squares = [n * n for n in numbers]
lengths = [len(name) for name in names]append()를 반복하는 루프보다 "무슨 결과를 만들고 있는가"가 더 빨리 보일 때가 많습니다.
일부만 남길 때는 뒤쪽 if를 읽으면 됩니다.
scores = [91, 77, 84, 59]
passed = [score for score in scores if score >= 80]이 형태는 원본 중 일부만 걸러 새 리스트를 만든다는 뜻입니다.
결과 값을 둘 중 하나로 바꾸고 싶으면 앞쪽 조건식을 씁니다.
labels = ["pass" if score >= 80 else "retry" for score in scores]같은 if가 들어가도 위치에 따라 뜻이 달라집니다.
- 뒤쪽
if: 결과에 남길지 말지 - 앞쪽 조건식: 어떤 값을 넣을지
가공과 필터를 한 줄에 같이 넣을 수도 있습니다.
labels = [name.strip().title() for name in raw_names if name.strip()]이런 식은 자주 쓰이지만, 한 줄 설명이 잘 안 될 만큼 길어지면 이미 읽기성이 떨어지기 시작한 것일 수 있습니다.
선택 기준
지금 리스트가 필요한가를 먼저 보면 됩니다.
squares = [n * n for n in range(1_000)]
square_iter = (n * n for n in range(1_000))리스트 컴프리헨션은 결과를 바로 메모리에 만들고, generator expression은 필요할 때 하나씩 꺼냅니다.
여러 번 재사용하거나 길이를 재야 하는 결과라면 리스트가 자연스럽고, 한 번만 흘려보낼 큰 데이터라면 generator expression이 더 맞을 수 있습니다.
부작용이 주인공이면 일반 for 루프가 더 낫습니다.
normalized = [name.strip() for name in names]
for name in names:
if not name:
continue
print(name)컴프리헨션은 새 결과를 만드는 데 강합니다. 로그 출력, 파일 저장, 조건 분기, 예외 처리처럼 본문 동작이 길어지면 일반 루프가 더 읽기 쉽습니다.
중첩이 깊어질수록 컴프리헨션을 고집하지 않는 편이 낫습니다.
이 정도는 읽을 수 있어도, 여기에 조건과 추가 가공까지 붙기 시작하면 빠르게 흐름을 잃기 쉽습니다. 한 줄에서 순서를 따라가기 어려우면 루프로 풀어쓰는 편이 보통 더 안전합니다.
주의할 점
리스트 컴프리헨션을 부작용 실행 도구처럼 쓰면 읽기성이 급격히 떨어집니다.
# 잘못된 예: append의 반환값(None)만 모음
result = [logs.append(name) for name in names]
# 올바른 예: 부작용이 목적이면 명시적인 for 루프
for name in names:
logs.append(name)리스트 결과가 목적이 아니라면 컴프리헨션보다 일반 루프가 더 직접적입니다.
필터와 조건식을 같은 것으로 읽으면 해석이 꼬입니다.
# 필터
[item for item in items if cond]
# 값 선택
[a if cond else b for item in items]둘 다 if가 보이지만, 하나는 항목을 남길지 말지 정하고, 다른 하나는 결과 값을 나눕니다. 이 차이를 놓치면 긴 컴프리헨션을 잘못 읽기 쉽습니다.
참고 링크
2 sources