본문 바로가기
Python

20. 데코레이터와 제너레이터 - 중급으로 가는 관문

by 샤나엘 2026. 2. 22.
반응형

데코레이터와 제너레이터

20. 데코레이터와 제너레이터 - 중급으로 가는 관문

함수를 감싸는 함수, 값을 하나씩 내보내는 함수. 파이썬의 데코레이터제너레이터는 처음엔 낯설지만, 한번 익히면 코드의 품격이 달라진다. 중급 파이썬으로 가는 관문을 열어보자.


일급 객체 (First-Class Object)

파이썬에서 함수는 일급 객체다. 즉, 함수를 변수에 담고, 인자로 전달하고, 반환값으로 쓸 수 있다.

# 1. 변수에 담기
def greet(name):
    return f"안녕, {name}!"

hello = greet           # 함수를 변수에 대입
print(hello("철수"))    # 안녕, 철수!

# 2. 함수를 인자로 전달
def apply(func, value):
    return func(value)

print(apply(len, "파이썬"))    # 3
print(apply(str.upper, "hello"))  # HELLO

# 3. 함수를 반환
def make_greeter(greeting):
    def greeter(name):
        return f"{greeting}, {name}!"
    return greeter

hi = make_greeter("안녕")
bye = make_greeter("잘가")
print(hi("철수"))     # 안녕, 철수!
print(bye("영희"))    # 잘가, 영희!

클로저 (Closure)

클로저는 내부 함수가 외부 함수의 변수를 기억하는 것이다.

def make_counter():
    count = 0
    def counter():
        nonlocal count    # 외부 함수의 변수 수정
        count += 1
        return count
    return counter

c = make_counter()
print(c())    # 1
print(c())    # 2
print(c())    # 3

💡 make_counter() 실행이 끝났는데도 count가 살아있다! 내부 함수 countercount기억하고 있기 때문이다.

def make_multiplier(n):
    def multiply(x):
        return x * n    # n을 기억!
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))    # 10
print(triple(5))    # 15

데코레이터 (Decorator)

데코레이터는 함수를 감싸서 기능을 추가하는 함수다.

데코레이터 없이 — 수동 방식

def say_hello():
    print("Hello!")

def add_greeting(func):
    def wrapper():
        print("===== 인사 시작 =====")
        func()
        print("===== 인사 끝 =====")
    return wrapper

# 수동으로 감싸기
say_hello = add_greeting(say_hello)
say_hello()
# ===== 인사 시작 =====
# Hello!
# ===== 인사 끝 =====

@ 문법 — 데코레이터 적용

def add_greeting(func):
    def wrapper():
        print("===== 인사 시작 =====")
        func()
        print("===== 인사 끝 =====")
    return wrapper

@add_greeting    # say_hello = add_greeting(say_hello) 와 같음
def say_hello():
    print("Hello!")

say_hello()
# ===== 인사 시작 =====
# Hello!
# ===== 인사 끝 =====

 

데코레이터 동작 흐름

인자가 있는 함수에 데코레이터 적용

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"[LOG] {func.__name__} 호출 - args: {args}, kwargs: {kwargs}")
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} 반환 → {result}")
        return result
    return wrapper

@log_call
def add(a, b):
    return a + b

@log_call
def greet(name, greeting="안녕"):
    return f"{greeting}, {name}!"

print(add(3, 5))
# [LOG] add 호출 - args: (3, 5), kwargs: {}
# [LOG] add 반환 → 8
# 8

print(greet("철수", greeting="하이"))
# [LOG] greet 호출 - args: ('철수',), kwargs: {'greeting': '하이'}
# [LOG] greet 반환 → 하이, 철수!
# 하이, 철수!

실전 데코레이터: 실행 시간 측정

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"[{func.__name__}] 실행 시간: {end - start:.4f}초")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "완료!"

@timer
def calculate_sum(n):
    return sum(range(n + 1))

print(slow_function())
# [slow_function] 실행 시간: 1.00xx초
# 완료!

print(calculate_sum(1000000))
# [calculate_sum] 실행 시간: 0.0xxx초
# 500000500000

실전 데코레이터: 호출 횟수 카운터

def count_calls(func):
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        print(f"[{func.__name__}] {wrapper.calls}번째 호출")
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@count_calls
def say_hi():
    print("Hi!")

say_hi()    # [say_hi] 1번째 호출 → Hi!
say_hi()    # [say_hi] 2번째 호출 → Hi!
say_hi()    # [say_hi] 3번째 호출 → Hi!

제너레이터 (Generator)

제너레이터yield 키워드를 사용해 값을 하나씩 생성하는 함수다. 일반 함수와 달리, 호출할 때마다 멈췄던 위치에서 다시 실행된다.

return vs yield

# 일반 함수 — 한 번에 모든 결과 반환
def get_numbers_list(n):
    result = []
    for i in range(n):
        result.append(i)
    return result

# 제너레이터 — 하나씩 생성
def get_numbers_gen(n):
    for i in range(n):
        yield i    # 값을 하나 내보내고 멈춤

# 사용
numbers_list = get_numbers_list(5)    # [0, 1, 2, 3, 4] 한 번에 메모리에
numbers_gen = get_numbers_gen(5)      # 제너레이터 객체 (아직 값 없음)

print(type(numbers_gen))    # <class 'generator'>

next()로 값 하나씩 꺼내기

gen = get_numbers_gen(3)

print(next(gen))    # 0 (첫 번째 yield에서 멈춤)
print(next(gen))    # 1 (두 번째 yield에서 멈춤)
print(next(gen))    # 2 (세 번째 yield에서 멈춤)
print(next(gen))    # StopIteration 에러!

for문으로 순회

def countdown(n):
    while n > 0:
        yield n
        n -= 1

for num in countdown(5):
    print(num, end=" ")
# 5 4 3 2 1

 

yield 실행 흐름


제너레이터의 장점: 메모리 효율

import sys

# 리스트: 메모리에 모든 값 저장
big_list = [i for i in range(1000000)]
print(f"리스트 크기: {sys.getsizeof(big_list):,} bytes")
# 리스트 크기: 8,448,728 bytes (약 8MB)

# 제너레이터: 값을 하나씩 생성
big_gen = (i for i in range(1000000))
print(f"제너레이터 크기: {sys.getsizeof(big_gen):,} bytes")
# 제너레이터 크기: 200 bytes (고정!)

💡 대용량 데이터를 다룰 때 제너레이터를 사용하면 메모리를 획기적으로 절약할 수 있다!


제너레이터 표현식

리스트 컴프리헨션의 []()로 바꾸면 제너레이터가 된다.

# 리스트 컴프리헨션
squares_list = [x ** 2 for x in range(10)]

# 제너레이터 표현식
squares_gen = (x ** 2 for x in range(10))

print(sum(squares_list))    # 285
print(sum(squares_gen))     # 285 (메모리 효율적)

실전 예시: 파일 읽기 제너레이터

def read_large_file(filepath):
    """대용량 파일을 한 줄씩 읽는 제너레이터"""
    with open(filepath, 'r', encoding='utf-8') as f:
        for line in f:
            yield line.strip()

# 사용 (파일이 10GB여도 메모리 걱정 없음!)
# for line in read_large_file("huge_data.txt"):
#     process(line)

무한 시퀀스

def infinite_counter(start=0):
    """무한히 숫자를 생성하는 제너레이터"""
    n = start
    while True:
        yield n
        n += 1

# 처음 10개만 사용
counter = infinite_counter(1)
for _ in range(10):
    print(next(counter), end=" ")
# 1 2 3 4 5 6 7 8 9 10

피보나치 제너레이터

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
for _ in range(10):
    print(next(fib), end=" ")
# 0 1 1 2 3 5 8 13 21 34

직접 해보기

문제 1. 함수 실행 전후에 "시작"과 "끝"을 출력하는 border 데코레이터를 만들어보자.

문제 2. 함수의 결과를 2배로 만드는 double_result 데코레이터를 만들어보자.

문제 3. 짝수만 생성하는 제너레이터 even_numbers(n)을 만들어보자. (0부터 n 미만)

문제 4. 제너레이터 표현식으로 1부터 100까지의 3의 배수의 합을 구해보자.

문제 5. 비밀번호를 무한히 생성하는 제너레이터를 만들어보자. (알파벳+숫자 8자리)

정답 보기
# 문제 1
def border(func):
    def wrapper(*args, **kwargs):
        print("=" * 30)
        print("시작")
        print("=" * 30)
        result = func(*args, **kwargs)
        print("=" * 30)
        print("끝")
        print("=" * 30)
        return result
    return wrapper
‍
@border
def say_hello(name):
    print(f"안녕하세요, {name}님!")
‍
say_hello("철수")
‍
# 문제 2
def double_result(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result * 2
    return wrapper
‍
@double_result
def add(a, b):
    return a + b
‍
print(add(3, 5))    # 16 (8의 2배)
‍
# 문제 3
def even_numbers(n):
    for i in range(0, n, 2):
        yield i
‍
for num in even_numbers(20):
    print(num, end=" ")
# 0 2 4 6 8 10 12 14 16 18
print()
‍
# 문제 4
result = sum(x for x in range(1, 101) if x % 3 == 0)
print(result)    # 1683
‍
# 문제 5
import random
import string
‍
def password_generator():
    chars = string.ascii_letters + string.digits
    while True:
        yield ''.join(random.choice(chars) for _ in range(8))
‍
gen = password_generator()
for _ in range(5):
    print(next(gen))

오늘의 정리

항목 내용
일급 객체 함수를 변수에 담고, 인자로 전달하고, 반환할 수 있음
클로저 내부 함수가 외부 함수의 변수를 기억하는 것
데코레이터 @데코레이터로 함수를 감싸서 기능 추가
yield 값을 하나 내보내고 함수 실행을 일시 정지
제너레이터 yield를 사용하는 함수. 메모리 효율적
제너레이터 표현식 (표현식 for x in 반복) — 소괄호 사용
실전 활용 타이머, 로깅, 호출 카운터, 대용량 파일 읽기

다음 편 예고: 모듈과 패키지 - 남이 만든 코드 활용하기

import로 다른 사람이 만든 코드를 가져다 쓰는 방법, 파이썬의 강력한 표준 라이브러리, 그리고 pip install로 외부 패키지를 설치하는 방법을 알아보자.


태그: 파이썬 Python 파이썬독학 데코레이터 decorator 제너레이터 generator yield 파이썬중급 IT교육

반응형

댓글