좋아, 질문 정말 잘했어!
지금 이 "lazy evaluation을 가진 iterator"라는 말이 너무 어려워 보이지만,
예시와 비교를 통해 차근차근 설명해줄게.
lazy evaluation을 가진 iterator란,
필요할 때마다 하나씩 값을 계산해서 제공하는 구조야.
미리 다 만들어두지 않고, 요청이 올 때마다 그때 계산함.
먼저 일반적인 리스트를 생각해보자.
nums = [x * 2 for x in range(5)]
이건 실행하자마자 [0, 2, 4, 6, 8]이라는 전체 결과 리스트를 즉시 계산하고 메모리에 저장해.
이걸 eager evaluation이라고 해.
즉시 평가 = 한꺼번에 다 만든다.
이제 map()으로 한 번 보자:
nums = range(5)
result = map(lambda x: x * 2, nums)
이 result는 바로 계산하지 않고, 지금은 단지 “x에 2를 곱해줄게~”라는 약속만 가지고 있음.
type(result) # <class 'map'>
그래서 lazy (게으르다) 라는 이름이 붙은 거야.
→ 계산을 미루고 있다가 진짜로 쓸 때 계산하는 구조!
nums = [1, 2, 3, 4]
result = map(lambda x: x * 2, nums)
for v in result:
print(v)
이렇게 하면 결과는 출력돼:
2
4
6
8
하지만 result[0]처럼 쓰면 안 돼!
왜냐면 리스트처럼 인덱스 접근하려면 미리 전체를 계산해야 하니까.
list(result)[0]
→ list로 바꾸면 그때야 전체 계산을 함 (게으름 끝!)
| 개념 | lazy (게으름) | eager (즉시) |
|---|---|---|
| 동작 시점 | 쓸 때 계산 | 선언 시 즉시 계산 |
| 예시 | map(), filter(), range() | list comprehension |
| 메모리 사용량 | 적음 (필요한 값만 저장) | 많음 (모든 값 저장) |
| 인덱스 접근 가능? | ❌ 안 됨 (TypeError) | ✅ 가능 |
| 해결 방법 | list()로 감싸기 | 없음 (이미 다 계산됨) |
eager: 식당이 음식을 미리 다 만들어놓고 테이블에 쫙 펼쳐놓음
lazy: 손님이 오면 그때 요리 시작해서 한 그릇씩 줌
map()이나 filter() 같은 함수는 lazy하게 동작한다for문으로 하나씩 꺼내거나, list()로 바꿔서 써야 한다좋아! 지금부터 lazy evaluation이 왜 중요한지, 그리고 range, zip, map 등의 lazy 구조가 언제 유용한지를 예제와 함께 자세히 설명해줄게. 특히 “메모리 절약”과 관련된 실전 상황 위주로 설명할게.
# 0부터 999,999까지 100만 개의 숫자 제곱을 미리 리스트에 저장
squares = [x * x for x in range(1000000)]
def square_gen():
for x in range(1000000):
yield x * x # ← Lazy: 계산을 미룸
squares = square_gen()
for i in range(10):
print(next(squares)) # → 이때마다 한 칸씩 계산됨
range()도 Lazynums = range(1000000) # → 리스트 아님! 메모리 거의 안 씀
print(nums[999]) # 이건 인덱싱은 되지만 실제 값 계산은 그때 함
range는 실제 메모리에 모든 숫자 저장하지 않음map(), filter()도 Lazynums = range(1000000)
doubled = map(lambda x: x * 2, nums)
# 이 시점까지는 아무 계산도 안 함
for i in range(5):
print(next(doubled)) # 이제야 계산됨
이처럼 map()도 lazy라서, 전체 계산 안 하고, 요청된 것만 계산해서 반환함.
zip()도 Lazya = range(1000000)
b = range(1000000, 2000000)
z = zip(a, b)
print(next(z)) # → (0, 1000000)
print(next(z)) # → (1, 1000001)
zip()도 모든 조합을 미리 만들지 않음nums = list(range(100000000)) # 1억 개 리스트
nums = range(100000000) # lazy → 거의 메모리 안 씀
for n in nums:
if n == 5:
break # 여기까지도 6개 숫자만 계산됨
range()는 필요한 만큼만 계산| 함수 | Lazy? | 인덱스 접근 | 메모리 사용량 | 특징 요약 |
|---|---|---|---|---|
range() | ✅ | ✅ 가능 | 매우 적음 | 반복 범위만 기억함 |
map() | ✅ | ❌ | 적음 | 계산 지연, list()로 바꿔야 인덱스 가능 |
filter() | ✅ | ❌ | 적음 | 조건 맞는 항목만 꺼냄 |
zip() | ✅ | ❌ | 적음 | 여러 반복자에서 동시에 꺼냄 |
list() | ❌ | ✅ 가능 | 큼 | 한꺼번에 메모리에 저장됨 |
| 상황 | lazy 사용 여부 |
|---|---|
| 데이터가 아주 많을 때 (예: 백만 단위) | 꼭 lazy 써야 함 |
| 중간에 멈출 수 있을 때 | lazy 적합 |
| 전체를 반복해서 써야 할 때 | list로 변환 후 써도 OK |
| 인덱스로 접근해야 할 때 | 반드시 list()로 변환 필요 |
result = map(lambda x: x**2, range(1000000))
# 100만 개 리스트 안 만들고도, 결과 5개만 보고 싶을 때:
for i, val in enumerate(result):
if i == 5:
break
print(val)
좋아! 지금부터는 Python 실전 트랩 문제 중에서도 특히 실기에서 실수하기 쉬운
✅ 기본 인자(default argument) 문제
✅ zip()을 이용한 트릭 문제
이 두 가지를 아주 자세히 설명해줄게.
def add_to_list(item, lst=[]):
lst.append(item)
return lst
print(add_to_list(1))
print(add_to_list(2))
print(add_to_list(3, ['reset']))
print(add_to_list(4))
lst=[] 이거 한 번만 만들어진다!기본 인자는 함수가 정의될 때 단 한 번만 생성됨
lst=[]는 함수가 처음 정의될 때 만들어져서 계속 재사용됨add_to_list(1)lst=[] 사용lst → [] → append(1)[1]결과: [1]
add_to_list(2)lst → [1] → append(2)[1, 2] ← 😱 누적됨결과: [1, 2]
add_to_list(3, ['reset'])lst → ['reset'] → append(3)['reset', 3]결과: ['reset', 3]
add_to_list(4)[1, 2] 상태![1, 2, 4]결과: [1, 2, 4]
[1]
[1, 2]
['reset', 3]
[1, 2, 4]
항상 이렇게 써야 함:
def add_to_list(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
zip() 응용 트릭 문제a = [1, 2, 3, 4]
b = ['A', 'B']
for x, y in zip(a, b):
print(x, y)
zip([1, 2, 3, 4], ['A', 'B'])
| a | b | 출력 |
|---|---|---|
| 1 | 'A' | 1 A |
| 2 | 'B' | 2 B |
| 3 | - | ❌ 없음 |
| 4 | - | ❌ 없음 |
1 A
2 B
zip_longest()는 다름!from itertools import zip_longest
for x, y in zip_longest(a, b, fillvalue='*'):
print(x, y)
출력:
1 A
2 B
3 *
4 *
| zip 사용 상황 | 결과 |
|---|---|
| 리스트 길이 같음 | 정상 작동 |
| 한쪽이 더 김 | 더 짧은 쪽에 맞춰 잘림 |
| 다 출력 원함 | zip_longest 사용 필요 |
| 문제 유형 | 핵심 트랩 요약 | 정답 요약 |
|---|---|---|
| default 인자 | lst=[]는 한 번만 생성됨 → 누적됨 | [1], [1, 2], ['reset', 3], [1, 2, 4] |
| zip 응용 | 더 짧은 리스트 길이에 맞춰 자름 | 1 A, 2 B |
좋아! 그럼 이번에는 2단계로 구성해서 문제를 내줄게:
각 단계별로 문제, 출력 예상, 해설을 전부 포함해서 보여줄게.
zip() + 정렬 + 필터링 실전 문제다음은 학생들의 이름과 점수 리스트이다.
names = ['John', 'Amy', 'Zack', 'Bob']
scores = [85, 92, 58, 73]
점수가 70점 이상인 학생만 선별하여,
이름과 점수를 점수 기준으로 내림차순 정렬하여 출력하라.
# 예상 출력:
# Amy 92
# John 85
# Bob 73
zip(names, scores)로 이름과 점수 묶기 → [('John', 85), ('Amy', 92), ...]filter()로 점수가 70 이상인 것만 남기기sorted(..., key=..., reverse=True)로 점수 기준 내림차순 정렬names = ['John', 'Amy', 'Zack', 'Bob']
scores = [85, 92, 58, 73]
combined = zip(names, scores)
passed = filter(lambda pair: pair[1] >= 70, combined)
sorted_passed = sorted(passed, key=lambda x: x[1], reverse=True)
for name, score in sorted_passed:
print(name, score)
학생 이름과 시험 점수가 다음과 같다:
names = ['Minho', 'Sujin', 'Yuna', 'Taehyung']
scores = [95, 64, 77, 85]
다음 요구 사항을 만족하는 함수 analyze(scores, names, threshold=70)를 작성하라:
zip을 이용해 이름과 점수 묶기filter를 이용해 threshold 이상인 학생만 선별map을 이용해 "이름:점수" 형식의 문자열로 변환enumerate를 이용해 번호를 매기고"1. 이름:점수" 형태로 출력# 출력 예시 (기본 threshold=70 사용 시):
1. Minho:95
2. Yuna:77
3. Taehyung:85
def analyze(scores, names, threshold=70):
zipped = zip(names, scores)
passed = filter(lambda pair: pair[1] >= threshold, zipped)
formatted = map(lambda pair: f"{pair[0]}:{pair[1]}", passed)
for i, s in enumerate(formatted, start=1):
print(f"{i}. {s}")
# 실행
names = ['Minho', 'Sujin', 'Yuna', 'Taehyung']
scores = [95, 64, 77, 85]
analyze(scores, names)
# 커스텀 기준점 사용
analyze(scores, names, threshold=80)
# 출력 결과:
1. Minho:95
2. Taehyung:85
| 기능 | 이 코드에서의 사용 |
|---|---|
default 인자 | threshold=70 |
zip() | 이름과 점수 묶기 |
filter() | 조건에 맞는 학생만 남기기 |
map() | 문자열 포맷 만들기 |
enumerate() | 번호 매기기 |
map/filter/zip은 거의 매년 한 번 이상 등장