출시·고도화 중
Python 안내서 · 6/6
Python다운(Pythonic) 코드란 언어가 제공하는 도구를 자연스럽게 활용해 짧고 읽기 쉽게 쓴 코드를 말합니다. 이 장에서는 그 바탕이 되는 반복자와 제너레이터, 컴프리헨션, 데코레이터를 살펴보고, Python다운 코드의 습관을 정리합니다.
for 문은 리스트뿐 아니라 문자열, 딕셔너리, 파일, range처럼 순회할 수 있는(iterable) 모든 객체에 쓸 수 있습니다. 내부적으로 for는 iter()로 반복자를 얻고, next()로 값을 하나씩 꺼내다가 StopIteration 예외가 나면 멈춥니다.
numbers = [10, 20, 30]
it = iter(numbers)
next(it) # 10
next(it) # 20
next(it) # 30
# 한 번 더 next(it) 를 부르면 StopIteration 이 납니다
# zip 과 enumerate 도 반복자를 돌려줍니다
names = ["mina", "joon"]
scores = [90, 85]
for rank, (name, score) in enumerate(zip(names, scores), start=1):
print(rank, name, score)반복자는 값을 필요할 때 하나씩 만들어 내므로, 모든 값을 메모리에 올리지 않고도 큰 데이터를 처리할 수 있습니다. 단, 한 번 끝까지 소비한 반복자는 다시 쓸 수 없습니다.
함수 안에 yield가 있으면 그 함수는 제너레이터 함수가 됩니다. 호출하면 본문이 바로 실행되지 않고 제너레이터 객체가 반환되며, 값을 요청할 때마다 다음 yield까지 실행하고 멈춥니다. 반복자 클래스를 직접 만드는 것보다 훨씬 간단합니다.
def countdown(n: int):
while n > 0:
yield n
n -= 1
list(countdown(3)) # [3, 2, 1]
def read_lines(path: str):
with open(path, encoding="utf-8") as f:
for line in f:
if line.strip():
yield line.rstrip("\n")
def numbered(lines):
for i, line in enumerate(lines, start=1):
yield f"{i}: {line}"
# 큰 파일도 한 줄씩 흘려 보내며 처리합니다
for row in numbered(read_lines("log.txt")):
print(row)다른 반복 가능한 객체의 값을 그대로 넘겨줄 때는 yield from을 씁니다. 표준 라이브러리의 itertools 모듈에는 islice, chain, groupby처럼 반복자를 조합하는 도구가 많습니다.
컴프리헨션은 반복과 조건을 한 식으로 표현해 새 리스트, 딕셔너리, 집합을 만드는 문법입니다. 괄호를 소괄호로 바꾸면 값을 미리 만들지 않는 제너레이터 식이 됩니다.
nums = range(10)
squares = [n * n for n in nums] # 리스트
evens = [n for n in nums if n % 2 == 0] # 조건 걸기
lengths = {w: len(w) for w in ["py", "rust"]} # 딕셔너리
initials = {w[0] for w in ["apple", "avocado"]} # 집합 {'a'}
total = sum(n * n for n in nums) # 제너레이터 식
pairs = [(x, y) for x in range(2) for y in range(2)]컴프리헨션은 짧을 때 가장 읽기 좋습니다. 조건과 반복이 여러 겹으로 쌓여 한눈에 읽히지 않는다면 평범한 for 문으로 풀어 쓰는 편이 낫습니다.
Python에서 함수는 값처럼 변수에 담고 인자로 넘길 수 있습니다. 데코레이터는 함수를 받아 기능을 덧붙인 새 함수를 돌려주는 함수이며, @이름 문법으로 적용합니다. 로깅, 실행 시간 측정, 권한 검사, 캐싱처럼 여러 함수에 공통으로 필요한 일을 분리할 때 씁니다.
import functools
import time
def timed(func):
@functools.wraps(func) # 원래 함수의 이름과 docstring 을 보존합니다
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__}: {time.perf_counter() - start:.4f}초")
return wrapper
@timed
def slow_add(a, b):
time.sleep(0.1)
return a + b
@functools.lru_cache(maxsize=None) # 표준 라이브러리의 결과 캐싱 데코레이터
def fib(n: int) -> int:
return n if n < 2 else fib(n - 1) + fib(n - 2)
slow_add(1, 2)
fib(80)@property, @staticmethod, @classmethod, @dataclass도 모두 데코레이터입니다.
REPL에서 import this를 실행하면 Python의 설계 철학을 담은 "The Zen of Python"이 출력됩니다. 실무에서는 다음 습관이 Python다운 코드로 이어집니다.
enumerate, 두 목록을 함께 돌 때는 zip을 씁니다.with로 열고, 정리는 컨텍스트 관리자에 맡깁니다.sum, any, all, sorted, max(key=...) 같은 내장 함수를 적극적으로 씁니다.users = [{"name": "mina", "active": True}, {"name": "joon", "active": False}]
# 덜 Python다운 방식
active = []
for i in range(len(users)):
if users[i]["active"] == True:
active.append(users[i]["name"])
# Python다운 방식
active = [u["name"] for u in users if u["active"]]
has_inactive = any(not u["active"] for u in users)for는 반복자 규약(iter, next) 위에서 동작하며, 반복자는 값을 필요할 때 하나씩 만듭니다.yield로 제너레이터를 만들면 큰 데이터를 메모리 걱정 없이 단계별로 처리할 수 있습니다.for 문으로 풀어 씁니다.functools.wraps를 꼭 씁니다.더 깊이 배우려면 다음 공식 문서를 참고합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.