레이지

류한선·2025년 6월 27일

실기연습-2

목록 보기
52/95

좋아, 질문 정말 잘했어!
지금 이 "lazy evaluation을 가진 iterator"라는 말이 너무 어려워 보이지만,
예시와 비교를 통해 차근차근 설명해줄게.


✅ 한 줄 요약부터

lazy evaluation을 가진 iterator란,
필요할 때마다 하나씩 값을 계산해서 제공하는 구조야.
미리 다 만들어두지 않고, 요청이 올 때마다 그때 계산함.


✅ 반대 개념: eager evaluation (즉시 평가)

먼저 일반적인 리스트를 생각해보자.

nums = [x * 2 for x in range(5)]

이건 실행하자마자 [0, 2, 4, 6, 8]이라는 전체 결과 리스트를 즉시 계산하고 메모리에 저장해.

이걸 eager evaluation이라고 해.
즉시 평가 = 한꺼번에 다 만든다.


✅ lazy 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 evaluation vs eager evaluation

개념lazy (게으름)eager (즉시)
동작 시점쓸 때 계산선언 시 즉시 계산
예시map(), filter(), range()list comprehension
메모리 사용량적음 (필요한 값만 저장)많음 (모든 값 저장)
인덱스 접근 가능?❌ 안 됨 (TypeError)✅ 가능
해결 방법list()로 감싸기없음 (이미 다 계산됨)

✅ 쉬운 비유

eager: 식당이 음식을 미리 다 만들어놓고 테이블에 쫙 펼쳐놓음
lazy: 손님이 오면 그때 요리 시작해서 한 그릇씩 줌


💬 결론

  • map()이나 filter() 같은 함수는 lazy하게 동작한다
  • 그래서 값을 직접 인덱스로 접근할 수 없다
  • 대신 for문으로 하나씩 꺼내거나, list()로 바꿔서 써야 한다

좋아! 지금부터 lazy evaluation이 왜 중요한지, 그리고 range, zip, map 등의 lazy 구조가 언제 유용한지예제와 함께 자세히 설명해줄게. 특히 “메모리 절약”과 관련된 실전 상황 위주로 설명할게.


✅ 1. 왜 Lazy Evaluation이 필요한가?

문제 상황:

# 0부터 999,999까지 100만 개의 숫자 제곱을 미리 리스트에 저장
squares = [x * x for x in range(1000000)]
  • ✅ 장점: 계산이 한 번에 끝난다
  • ❌ 단점: 100만 개 값을 한꺼번에 메모리에 저장함 → 메모리 폭발 가능

✅ 2. Lazy Evaluation으로 바꿔보기

def square_gen():
    for x in range(1000000):
        yield x * x  # ← Lazy: 계산을 미룸

squares = square_gen()
  • 이 코드는 실행해도 아무것도 계산 안 함
  • next() 혹은 for문으로 꺼낼 때만 1개씩 계산
for i in range(10):
    print(next(squares))  # → 이때마다 한 칸씩 계산됨

✅ 3. range()도 Lazy

nums = range(1000000)  # → 리스트 아님! 메모리 거의 안 씀
print(nums[999])       # 이건 인덱싱은 되지만 실제 값 계산은 그때 함
  • range실제 메모리에 모든 숫자 저장하지 않음
  • 시작 값, 끝 값, 간격 정보만 저장
    필요할 때마다 계산해서 꺼내줌

✅ 4. map(), filter()도 Lazy

nums = range(1000000)
doubled = map(lambda x: x * 2, nums)

# 이 시점까지는 아무 계산도 안 함

for i in range(5):
    print(next(doubled))  # 이제야 계산됨

이처럼 map()lazy라서, 전체 계산 안 하고, 요청된 것만 계산해서 반환함.


✅ 5. zip()도 Lazy

a = range(1000000)
b = range(1000000, 2000000)

z = zip(a, b)

print(next(z))  # → (0, 1000000)
print(next(z))  # → (1, 1000001)
  • zip()모든 조합을 미리 만들지 않음
  • 각 반복자에서 한 번씩 꺼내서 짝을 지어줌

✅ 6. 실전 예시: 메모리 폭발 차이 보기

❌ 잘못된 방식 (메모리 많이 씀)

nums = list(range(100000000))  # 1억 개 리스트
  • 이건 메모리에 1억 개 숫자를 저장함
  • 수 GB의 메모리 필요 → 프로그램 터질 수 있음

✅ 효율적인 방식 (lazy 사용)

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()을 이용한 트릭 문제
이 두 가지를 아주 자세히 설명해줄게.


✅ 1. Python 기본 인자(default argument) 트랩 문제


🔢 문제 코드:

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=[]는 함수가 처음 정의될 때 만들어져서 계속 재사용됨

🧠 한 줄씩 흐름 보기

1️⃣ add_to_list(1)

  • 기본 인자 lst=[] 사용
  • lst → [] → append(1)
  • return: [1]
결과: [1]

2️⃣ add_to_list(2)

  • 또 lst 인자를 생략했으므로 → 이전에 쓰인 리스트 재사용됨!
  • lst → [1] → append(2)
  • return: [1, 2] ← 😱 누적됨
결과: [1, 2]

3️⃣ add_to_list(3, ['reset'])

  • 이번엔 lst를 명시적으로 전달 → 새로운 리스트 사용
  • lst → ['reset'] → append(3)
  • return: ['reset', 3]
결과: ['reset', 3]

4️⃣ add_to_list(4)

  • 다시 기본 인자 lst=[] 사용 → 여전히 [1, 2] 상태!
  • append(4) → [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

✅ 2. Python zip() 응용 트릭 문제


🔢 문제 코드:

a = [1, 2, 3, 4]
b = ['A', 'B']

for x, y in zip(a, b):
    print(x, y)

❓ 출력 결과는?


🔍 zip의 동작 방식

zip([1, 2, 3, 4], ['A', 'B'])
  • zip은 가장 짧은 리스트 기준으로 동작한다
    → 더 긴 리스트는 잘려나감

🔁 zip 동작 순서

ab출력
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 사용 상황결과
리스트 길이 같음정상 작동
한쪽이 더 김더 짧은 쪽에 맞춰 잘림
다 출력 원함zip_longest 사용 필요

✅ 마무리 요약

문제 유형핵심 트랩 요약정답 요약
default 인자lst=[]는 한 번만 생성됨 → 누적됨[1], [1, 2], ['reset', 3], [1, 2, 4]
zip 응용더 짧은 리스트 길이에 맞춰 자름1 A, 2 B

좋아! 그럼 이번에는 2단계로 구성해서 문제를 내줄게:


✅ 1단계: zip을 이용한 리스트 묶기 + 정렬 + 필터링 실전 문제

✅ 2단계: default 인자 + zip + map + filter + enumerate가 다 들어간 고난이도 문제

각 단계별로 문제, 출력 예상, 해설을 전부 포함해서 보여줄게.


✅ 1단계: zip() + 정렬 + 필터링 실전 문제


🔢 문제

다음은 학생들의 이름과 점수 리스트이다.

names = ['John', 'Amy', 'Zack', 'Bob']
scores = [85, 92, 58, 73]

점수가 70점 이상인 학생만 선별하여,
이름과 점수를 점수 기준으로 내림차순 정렬하여 출력하라.

# 예상 출력:
# Amy 92
# John 85
# Bob 73

🧠 풀이 흐름

  1. zip(names, scores)로 이름과 점수 묶기 → [('John', 85), ('Amy', 92), ...]
  2. filter()로 점수가 70 이상인 것만 남기기
  3. sorted(..., key=..., reverse=True)로 점수 기준 내림차순 정렬
  4. for문으로 출력

✅ 정답 코드

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)

✅ 2단계: default 인자 + zip + map + filter + enumerate 총집합 문제


🔢 문제

학생 이름과 시험 점수가 다음과 같다:

names = ['Minho', 'Sujin', 'Yuna', 'Taehyung']
scores = [95, 64, 77, 85]

다음 요구 사항을 만족하는 함수 analyze(scores, names, threshold=70)를 작성하라:

  1. zip을 이용해 이름과 점수 묶기
  2. filter를 이용해 threshold 이상인 학생만 선별
  3. map을 이용해 "이름:점수" 형식의 문자열로 변환
  4. enumerate를 이용해 번호를 매기고
  5. "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은 거의 매년 한 번 이상 등장
  • lambda, default 인자, enumerate를 조합하면 함수형 스타일까지 가능함

0개의 댓글