1~3주차 — 타입 힌트와 dataclass · 데코레이터와 컨텍스트 매니저 · 이터레이터·제너레이터 심화
타입 힌트 문법, typing 모듈(Optional/Union/Callable), 그리고 보일러플레이트를 줄여주는 @dataclass 와 frozen 옵션까지 — 견고한 코드의 출발점입니다. 함수 데코레이터, 인자 받는 데코레이터, 클래스/함수 기반 컨텍스트 매니저까지 — 횡단 관심사를 깔끔히 분리하는 두 도구를 익힙니다. for 루프 뒤에 숨은 __iter__/__next__ 프로토콜, yield from 위임, itertools 의 강력한 함수들, 그리고 코루틴 기초까지 다룹니다.
이 강의에서 배우는 것
- 1타입 힌트 문법(int, str, list[int], Optional)을 익힌다
- 2mypy 로 정적 타입을 검사한다
- 3@dataclass 로 데이터 클래스를 간결히 작성한다
- 4field(default_factory=list) 의 의의를 안다
- 5함수 데코레이터를 정의하고 적용한다
- 6인자 받는 데코레이터를 만든다
- 7클래스/함수 기반 컨텍스트 매니저를 구현한다
- 8@functools.wraps 의 역할을 안다
- 9이터레이터 프로토콜(__iter__, __next__)을 직접 구현한다
- 10yield from 으로 제너레이터를 위임한다
- 11itertools 의 함수들을 활용한다
- 12send 기반 코루틴 기초를 안다
1주차 — 타입 힌트와 dataclass
타입 힌트 문법, typing 모듈(Optional/Union/Callable), 그리고 보일러플레이트를 줄여주는 @dataclass 와 frozen 옵션까지 — 견고한 코드의 출발점입니다.
1. 타입 힌트 기본
런타임에는 영향이 없지만, IDE 자동완성·정적 분석·문서화에 큰 도움이 됩니다.
def greet(name: str, age: int = 20) -> str:
return f"{name}({age})"
count: int = 0
items: list[str] = []2. typing 모듈
from typing import Optional, Union, Any, Callable
def find_user(uid: int) -> Optional[dict]:
"""Optional[X] = X | None"""
...
def parse(value: Union[str, int]) -> int:
"""str 또는 int (Python 3.10+ 에서는 str | int)"""
...
handler: Callable[[int], str] = lambda x: str(x)Python 3.10+ 부터는 더 짧게 쓸 수 있습니다.
def find_user(uid: int) -> dict | None: ...
def parse(value: str | int) -> int: ...3. 컨테이너 타입
nums: list[int] = [1, 2, 3]
scores: dict[str, int] = {"Alice": 90}
pair: tuple[int, str] = (1, "a")
unique: set[int] = {1, 2, 3}4. mypy
pip install mypy
mypy main.py
mypy --strict main.py # 엄격 모드5. dataclass
__init__, __repr__, __eq__ 가 자동 생성됩니다.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p) # Point(x=1, y=2)
print(p == Point(1, 2)) # True6. dataclass 옵션
from dataclasses import dataclass, field
@dataclass
class Book:
title: str
author: str
tags: list[str] = field(default_factory=list)
price: float = 0.0
b = Book("Python", "Guido")
b.tags.append("language")tags: list[str] = [] 는 모든 인스턴스가 공유합니다. 절대 안 됨 — 반드시 field(default_factory=list).
7. frozen dataclass
불변 객체. 해시 가능 → set/dict 키로 사용 가능합니다.
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
# p.x = 99 # FrozenInstanceError
{Point(1, 2)} # OK자주 하는 실수
- 가변 기본값을 그대로 — field(default_factory=list) 사용
- Optional 의미 오해 — Optional[int] = int | None. '선택적 인자'가 아님
- 타입 힌트가 런타임에 검증된다고 오해 — 그냥 힌트일 뿐. 런타임 검증은 pydantic
FAQ
Q1. 타입 힌트를 다 달아야 하나요? — 공개 API(함수 시그니처)는 강력 권장. 내부 변수는 추론으로 충분.
Q2. dataclass vs pydantic.BaseModel — dataclass는 표준, pydantic은 런타임 검증·직렬화 강력. 데이터 처리 많으면 pydantic.
Q3. Any 를 쓰면? — mypy가 검사를 포기. 가능하면 더 구체적인 타입.
2주차 — 데코레이터와 컨텍스트 매니저
함수 데코레이터, 인자 받는 데코레이터, 클래스/함수 기반 컨텍스트 매니저까지 — 횡단 관심사를 깔끔히 분리하는 두 도구를 익힙니다.
1. 데코레이터란
함수를 인자로 받아 새로운 함수를 반환합니다. 로깅·캐싱·인증 같은 기능을 횡단으로 추가.
def loud(func):
def wrapper(*args, **kwargs):
print(f">>> {func.__name__} 호출")
result = func(*args, **kwargs)
print(f"<<< 결과: {result}")
return result
return wrapper
@loud
def add(a, b):
return a + b
add(3, 5)
# >>> add 호출
# <<< 결과: 8@loud 는 add = loud(add) 의 문법 설탕입니다.
2. functools.wraps
데코레이터로 감싸도 원본 함수 이름·docstring 을 유지합니다.
from functools import wraps
def loud(func):
@wraps(func) # 이거 없으면 .__name__ 이 'wrapper' 됨
def wrapper(*args, **kwargs):
...
return wrapper3. 인자 받는 데코레이터
데코레이터를 만드는 함수 — 3중 함수 구조.
def retry(times):
def deco(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"재시도 {i+1}/{times}: {e}")
raise
return wrapper
return deco
@retry(3)
def fragile():
...4. 컨텍스트 매니저
with 문에서 동작합니다. 자원 획득·해제를 안전하게.
클래스 기반:
class FileLock:
def __init__(self, path):
self.path = path
def __enter__(self):
print(f"잠금: {self.path}")
return self
def __exit__(self, exc_type, exc, tb):
print("해제")
with FileLock("data.txt"):
print("작업 중")함수 기반 (@contextmanager):
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter()
yield
print(f"{label}: {time.perf_counter() - start:.4f}s")
with timer("작업"):
sum(range(1_000_000))5. 실전 — 메모이제이션
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
fib(100) # 빠름자주 하는 실수
- @wraps 누락 — 디버깅·문서가 망가짐. 항상 붙이기
- 데코레이터가 return 안 함 — 결과가 None
- @contextmanager 함수에 yield 가 두 개 이상 — 한 번만
- __exit__ 가 True 반환 — 예외를 삼킴
FAQ
Q1. 데코레이터는 언제? — 여러 함수에 같은 부가 동작(로깅/인증/캐싱)을 넣을 때.
Q2. with 와 try-finally 의 차이? — with 는 정형화된 try-finally. 가독성·실수 방지.
Q3. 데코레이터를 여러 개 쌓으면? — 아래쪽이 먼저 적용. @A @B def f → f = A(B(f)).
3주차 — 이터레이터·제너레이터 심화
for 루프 뒤에 숨은 __iter__/__next__ 프로토콜, yield from 위임, itertools 의 강력한 함수들, 그리고 코루틴 기초까지 다룹니다.
1. 이터레이터 프로토콜
for ... in x 는 사실 iter(x) → next(it) 반복입니다.
class Range:
def __init__(self, start, stop):
self.cur = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.cur >= self.stop:
raise StopIteration
self.cur += 1
return self.cur - 1
print(list(Range(1, 5))) # [1, 2, 3, 4]2. yield from
다른 이터러블을 그대로 yield. 평탄화에 유용합니다.
def chain(*iters):
for it in iters:
yield from it
print(list(chain([1, 2], (3, 4), "ab"))) # [1, 2, 3, 4, 'a', 'b']3. itertools
표준 라이브러리의 보석.
import itertools as it
# 무한 시퀀스
counter = it.count(1, 2) # 1, 3, 5, 7, ...
print(list(it.islice(counter, 5))) # [1, 3, 5, 7, 9]
# 조합/순열
print(list(it.combinations([1, 2, 3, 4], 2)))
print(list(it.permutations([1, 2, 3], 2)))
# 그룹
data = [("a", 1), ("a", 2), ("b", 3), ("b", 4)]
for key, group in it.groupby(data, key=lambda x: x[0]):
print(key, list(group))
# 사전 곱
print(list(it.product([1, 2], "ab")))
# 누적
print(list(it.accumulate([1, 2, 3, 4]))) # [1, 3, 6, 10]4. 코루틴 기초
yield 가 양방향 통신 — send(값) 으로 값 주입.
def echo():
while True:
x = yield
print(f"받음: {x}")
co = echo()
next(co) # 첫 yield까지 진행 (필수)
co.send("hello")
co.send("world")Python 3.5+ 부터는 async/await 가 더 일반적 — 다음 주차에서.
5. 제너레이터 vs 리스트
| 제너레이터 | 리스트 | |
|---|---|---|
| 메모리 | 항목 1개씩 | 전체 |
| 길이 | 모름 | len() |
| 재사용 | 한 번만 | 여러 번 |
| 인덱싱 | 안 됨 | 됨 |
자주 하는 실수
- 이터레이터를 두 번 순회 — 두 번째는 빈 결과. 다시 생성 필요
- __next__ 에서 StopIteration 누락 — 무한 루프
- groupby 전 정렬 안 함 — groupby는 연속된 같은 키만 묶음
FAQ
Q1. 이터레이터와 제너레이터의 차이? — 제너레이터는 이터레이터의 한 종류. yield 로 만든 것.
Q2. itertools 보다 more-itertools 가 좋다는데? — 표준은 아니지만 강력. 실무에서 자주 사용.
💻 예제 (examples)
실제로 실행해 결과를 확인할 수 있는 예제 코드입니다.
def greet(name: str, age: int = 20) -> str:
return f"{name}({age})"
count: int = 0
items: list[str] = ["사과", "바나나"]
print(greet("홍길동"))
print(greet("김파이", 28))
print(count, items)홍길동(20)
김파이(28)
0 ['사과', '바나나']from typing import Optional, Callable
def find_user(uid: int) -> Optional[dict]:
return {"id": 1, "name": "Alice"} if uid == 1 else None
def apply(f: Callable[[int], int], x: int) -> int:
return f(x)
print(find_user(1))
print(find_user(99))
print(apply(lambda n: n * n, 5)){'id': 1, 'name': 'Alice'}
None
25from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1)
print(p1 == p2)Point(x=1, y=2)
Truefrom dataclasses import dataclass, field
@dataclass
class Book:
title: str
tags: list[str] = field(default_factory=list)
@dataclass(frozen=True)
class Coord:
x: int
y: int
b = Book("Python")
b.tags.append("language")
print(b)
print({Coord(1, 2), Coord(1, 2), Coord(3, 4)})Book(title='Python', tags=['language'])
{Coord(x=1, y=2), Coord(x=3, y=4)}from functools import wraps
def loud(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f">>> {func.__name__} 호출")
result = func(*args, **kwargs)
print(f"<<< 결과: {result}")
return result
return wrapper
@loud
def add(a, b):
return a + b
add(3, 5)
print("이름:", add.__name__)>>> add 호출
<<< 결과: 8
이름: addfrom functools import wraps
def retry(times):
def deco(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"재시도 {i+1}/{times}: {e}")
raise
return wrapper
return deco
count = 0
@retry(3)
def fragile():
global count
count += 1
if count < 3:
raise RuntimeError("일시 오류")
return "성공"
print(fragile())재시도 1/3: 일시 오류
재시도 2/3: 일시 오류
성공class FileLock:
def __init__(self, path):
self.path = path
def __enter__(self):
print(f"잠금: {self.path}")
return self
def __exit__(self, exc_type, exc, tb):
print("해제")
with FileLock("data.txt"):
print("작업 중")잠금: data.txt
작업 중
해제from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter()
yield
print(f"{label}: {time.perf_counter() - start:.4f}s")
with timer("계산"):
sum(i * i for i in range(1_000_000))계산: 0.0521sclass Range:
def __init__(self, start, stop):
self.cur = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.cur >= self.stop:
raise StopIteration
self.cur += 1
return self.cur - 1
print(list(Range(1, 5)))[1, 2, 3, 4]def chain(*iters):
for it in iters:
yield from it
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
print(list(chain([1, 2], (3, 4), "ab")))
print(list(flatten([1, [2, [3, [4, 5]]], 6])))[1, 2, 3, 4, 'a', 'b']
[1, 2, 3, 4, 5, 6]import itertools as it
print(list(it.islice(it.count(1, 2), 5)))
print(list(it.combinations([1, 2, 3, 4], 2)))
data = sorted([("b", 3), ("a", 1), ("a", 2)], key=lambda x: x[0])
for key, group in it.groupby(data, key=lambda x: x[0]):
print(key, list(group))
print(list(it.accumulate([1, 2, 3, 4])))[1, 3, 5, 7, 9]
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
a [('a', 1), ('a', 2)]
b [('b', 3)]
[1, 3, 6, 10]def echo():
while True:
x = yield
print(f"받음: {x}")
co = echo()
next(co)
co.send("hello")
co.send("world")
co.close()받음: hello
받음: world📝 과제 (exercises)
직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.
함수에 타입 힌트
목표: 기본 함수 시그니처에 타입 힌트를 추가한다.
- add(a: int, b: int) -> int 타입 힌트 부여
- join(words: list[str], sep: str = ', ') -> str 타입 힌트 부여
기본값이 있어도 타입 힌트는 콜론 뒤에 그대로 쓴다
5
사과, 바나나, 포도- · mypy main.py 가 오류 없이 통과
- · 함수가 정상 동작
▶정답 코드 펼치기 / 접기
def add(a: int, b: int) -> int:
return a + b
def join(words: list[str], sep: str = ", ") -> str:
return sep.join(words)
print(add(2, 3))
print(join(["사과", "바나나", "포도"]))5
사과, 바나나, 포도Book/Member dataclass
목표: 도서관 도메인을 dataclass 로 모델링한다.
- @dataclass Book(title, author, tags: list[str] = field(default_factory=list))
- @dataclass Member(name, age: int)
- 두 인스턴스를 만들어 출력
가변 기본값은 field(default_factory=list)
Book(title='파이썬', author='홍길동', tags=['언어'])
Member(name='Alice', age=25)- · dataclass 데코레이터 사용
- · field 사용 정확
- · 출력 일치
▶정답 코드 펼치기 / 접기
from dataclasses import dataclass, field
@dataclass
class Book:
title: str
author: str
tags: list[str] = field(default_factory=list)
@dataclass
class Member:
name: str
age: int
b = Book("파이썬", "홍길동")
b.tags.append("언어")
m = Member("Alice", 25)
print(b)
print(m)Book(title='파이썬', author='홍길동', tags=['언어'])
Member(name='Alice', age=25)Optional 시그니처
목표: Optional 을 사용하는 함수 5개를 작성한다.
- find_user(uid: int) -> Optional[dict]
- first(items: list[int]) -> Optional[int]
- div(a: int, b: int) -> Optional[float]
- get_age(name: str) -> Optional[int]
- parse_int(s: str) -> Optional[int]
반환할 값이 없으면 None 반환
{'id': 1}
1
None
None
42- · 5개 모두 Optional 반환 타입
- · None 반환 케이스 처리
▶정답 코드 펼치기 / 접기
from typing import Optional
DB = {1: {"id": 1}, 2: {"id": 2}}
AGES = {"Alice": 25}
def find_user(uid: int) -> Optional[dict]:
return DB.get(uid)
def first(items: list[int]) -> Optional[int]:
return items[0] if items else None
def div(a: int, b: int) -> Optional[float]:
return None if b == 0 else a / b
def get_age(name: str) -> Optional[int]:
return AGES.get(name)
def parse_int(s: str) -> Optional[int]:
try:
return int(s)
except ValueError:
return None
print(find_user(1))
print(first([1, 2, 3]))
print(div(10, 0))
print(get_age("Bob"))
print(parse_int("42")){'id': 1}
1
None
None
42@logged 데코레이터
목표: 함수 호출과 결과를 로깅하는 데코레이터를 만든다.
- @logged 적용 시 인자와 결과를 한 줄에 print
- @wraps 로 메타데이터 보존
wrapper 안에서 args, kwargs 출력 후 결과 반환
add(2, 3) -> 5
add(10, 20) -> 30- · wraps 사용
- · 출력 형식 일치
▶정답 코드 펼치기 / 접기
from functools import wraps
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f"{func.__name__}{args} -> {result}")
return result
return wrapper
@logged
def add(a, b):
return a + b
add(2, 3)
add(10, 20)add(2, 3) -> 5
add(10, 20) -> 30@cached 메모이제이션
목표: 결과를 캐시하는 데코레이터를 직접 구현한다.
- @cached 가 같은 인자에 대해 함수 본체를 재실행하지 않는다
- fib(30) 등 큰 입력에서 즉시 반환
dict 로 args 키 → 결과 저장
832040- · 인자 동일하면 캐시 사용
- · 정확한 값 반환
▶정답 코드 펼치기 / 접기
from functools import wraps
def cached(func):
store = {}
@wraps(func)
def wrapper(*args):
if args not in store:
store[args] = func(*args)
return store[args]
return wrapper
@cached
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(30))832040with timer 컨텍스트 매니저
목표: @contextmanager 로 실행 시간을 측정한다.
- with timer('label'): ... 형식
- 블록 종료 시 경과 초를 출력
time.perf_counter()
yield 전후로 시각 측정
합계: 0.05s 정도- · @contextmanager 사용
- · 예외 시에도 시간 출력 (선택)
▶정답 코드 펼치기 / 접기
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
print(f"{label}: {time.perf_counter() - start:.2f}s")
with timer("합계"):
s = sum(i for i in range(5_000_000))
print(s)합계: 0.21s
12499997500000chunked 제너레이터
목표: 리스트를 n개씩 잘라서 yield 하는 제너레이터를 만든다.
- chunked(iterable, n) -> generator
- 마지막 청크는 n보다 작아도 됨
슬라이싱 또는 itertools.islice 사용
[1, 2, 3]
[4, 5, 6]
[7]- · 제너레이터 함수
- · 정확한 청크 분할
▶정답 코드 펼치기 / 접기
def chunked(iterable, n):
buf = []
for x in iterable:
buf.append(x)
if len(buf) == n:
yield buf
buf = []
if buf:
yield buf
for c in chunked([1, 2, 3, 4, 5, 6, 7], 3):
print(c)[1, 2, 3]
[4, 5, 6]
[7]groupby 로 그룹화
목표: (date, message) 튜플 리스트를 날짜별로 그룹화한다.
- itertools.groupby 사용
- 그룹 전 정렬 필수
sorted(data, key=lambda x: x[0])
2026-05-09: 2건
2026-05-10: 1건- · groupby 사용
- · 정확한 카운트
▶정답 코드 펼치기 / 접기
import itertools as it
logs = [
("2026-05-10", "INFO"),
("2026-05-09", "ERROR"),
("2026-05-09", "WARN"),
]
logs.sort(key=lambda x: x[0])
for date, group in it.groupby(logs, key=lambda x: x[0]):
print(f"{date}: {len(list(group))}건")2026-05-09: 2건
2026-05-10: 1건무한 피보나치 + islice
목표: 무한 피보나치 제너레이터에서 처음 N개를 꺼낸다.
- fibs() 가 무한 제너레이터
- itertools.islice 로 N개 추출
a, b = b, a + b 패턴
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]- · 무한 yield
- · islice 사용
- · 정확한 N개
▶정답 코드 펼치기 / 접기
import itertools as it
def fibs():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(it.islice(fibs(), 10)))[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.
댓글은 회원만 쓸 수 있습니다.
광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침