1 Python Decorator
파이썬 데코레이터는 함수나 클래스를 수정하지 않고도 기능을 추가하거나 확장할 수 있게 해주는 강력한 문법이다. 데코레이터를 사용하면 코드의 재사용성을 높이고, 관심사의 분리(Separation of Concerns)를 통해 더 깔끔한 코드를 작성할 수 있다.
1.1 데코레이터의 정의
데코레이터는 다른 함수를 인자로 받아서 그 함수의 기능을 확장하거나 수정한 새로운 함수를 반환하는 함수다. @decorator_name 형태로 함수 위에 선언하여 사용한다.
1.2 데코레이터의 작동 원리
데코레이터는 실제로 다음과 같은 과정을 거친다:
# 데코레이터 정의
def my_decorator(func):
def wrapper(*args, **kwargs):
# 여기에 감싸기 전후 작업 삽입
result = func(*args, **kwargs)
return result
return wrapper
# 데코레이터 사용
@my_decorator
def my_function():
pass
# 데코레이터 사용 코드는 아래와 동일하다
def my_function():
pass
my_function = my_decorator(my_function)- 데코레이터는 “함수 안에 함수를 정의하고, 그 하위 함수(wrapper)를 리턴하는 구조
- my_decorator는 인자로 받은 함수(my_function)를 감싸고, 감싼 함수를 반환
1.3 데코레이터 예제
1.4 인자를 받는 함수를 위한 데코레이터
def args_decorator(func):
def wrapper(*args, **kwargs):
print(f"함수 {func.__name__} 호출됨")
print(f"인자: {args}, 키워드 인자: {kwargs}")
result = func(*args, **kwargs) #*args: 위치 인자 (튜플로 들어옴), **kwargs: 키워드 인자 (딕셔너리로 들어옴)
print(f"결과: {result}")
return result
return wrapper
@args_decorator
def add(a, b):
return a + b
result = add(3, 5)
# 함수 add 호출됨
# 인자: (3, 5), 키워드 인자: {}
# 결과: 8
@args_decorator
def greet(name, greeting="안녕하세요"):
return f"{greeting}, {name}님!"
greet(name="태훈", greeting="반갑습니다")
# 함수 greet 호출됨
# 인자: (), 키워드 인자: {'name': '태훈', 'greeting': '반갑습니다'}
# 결과: 반갑습니다, 태훈님!
@args_decorator
def order(item, quantity, price=None, customer=None):
result = f"{customer}님이 {item} {quantity}개를 주문했습니다."
if price is not None:
result += f" 총 가격은 {quantity * price}원입니다."
return result
order("커피", 2, price=4500, customer="민수")
# 함수 order 호출됨
# 인자: ('커피', 2), 키워드 인자: {'price': 4500, 'customer': '민수'}
# 결과: 민수님이 커피 2개를 주문했습니다. 총 가격은 9000원입니다.1.5 데코레이터의 장점
- 코드 재사용성: 공통 기능을 여러 함수에 쉽게 적용할 수 있다.
- 관심사 분리: 비즈니스 로직과 부가 기능을 분리할 수 있다.
- 가독성: 함수의 핵심 로직에 집중할 수 있다.
- 유지보수성: 공통 기능의 수정이 용이하다.
1.6 데코레이터 사용 시 주의사항
- 성능: 데코레이터는 함수 호출에 오버헤드를 추가한다.
- 디버깅: 데코레이터가 적용된 함수는 디버깅이 복잡할 수 있다.
- 메타데이터:
functools.wraps를 사용하지 않으면 원본 함수 정보가 손실된다.
2 결론
- 파이썬 데코레이터는 함수의 기능을 확장하고 코드의 재사용성을 높이는 강력한 도구다.
- 로깅, 인증, 성능 측정, 캐싱 등의 공통 기능을 효율적으로 구현할 수 있으며, 관심사의 분리를 통해 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있다.
- 다양한 형태의 데코레이터를 이해하고 적절히 활용한다면 파이썬 프로그래밍의 효율성과 품질을 크게 향상시킬 수 있다.
3 실용적인 데코레이터 예제
3.1 실행 시간 측정 데코레이터
- 실행 시간 측정 데코레이터는 함수의 실행 시간을 측정하여 성능 모니터링을 용이하게 한다.
import time
from functools import wraps
def timing_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} 실행 시간: {end_time - start_time:.4f}초")
return result
return wrapper
@timing_decorator
def slow_function():
time.sleep(1)
return "완료"
slow_function()
# slow_function 실행 시간: 1.0000초
# 완료3.2 로깅 데코레이터
- 로깅 데코레이터는 함수의 호출 정보를 기록하여 디버깅과 모니터링을 용이하게 한다.
import logging
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"{func.__name__} 함수가 호출되었습니다.")
try:
result = func(*args, **kwargs)
logging.info(f"{func.__name__} 함수가 성공적으로 완료되었습니다.")
return result
except Exception as e:
logging.error(f"{func.__name__} 함수에서 오류 발생: {e}")
raise
return wrapper
@log_calls
def divide(a, b):
return a / b
divide(10, 2)
# divide 함수가 호출되었습니다.
# divide 함수가 성공적으로 완료되었습니다.
# 5.03.3 캐싱 데코레이터
- 캐싱 데코레이터는 함수의 결과를 메모리에 저장하여 중복 계산을 피하고 성능을 향상시킨다.
from functools import wraps
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args in cache:
print(f"캐시에서 결과 반환: {args}")
return cache[args]
result = func(*args)
cache[args] = result
print(f"결과 캐시에 저장: {args} -> {result}")
return result
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
fibonacci(10)
# 결과 캐시에 저장: (10,) -> 55
# 55 3.4 매개변수를 받는 데코레이터
- 매개변수를 받는 데코레이터는 함수의 호출 횟수를 카운트하거나 반복 실행을 제어할 때 유용하다.
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
print(f"실행 {i+1}회:")
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"안녕하세요, {name}님!")
# 실행
greet("홍길동")
# 출력:
# 실행 1회:
# 안녕하세요, 홍길동님!
# 실행 2회:
# 안녕하세요, 홍길동님!
# 실행 3회:
# 안녕하세요, 홍길동님!3.5 클래스 기반 데코레이터
- 클래스 기반 데코레이터는 함수의 호출 횟수를 카운트하거나 반복 실행을 제어할 때 유용하다.
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} 함수가 {self.count}번 호출되었습니다.")
return self.func(*args, **kwargs)
@CountCalls
def say_something():
print("무언가를 말합니다.")
# 실행
say_something() # say_something 함수가 1번 호출되었습니다.
say_something() # say_something 함수가 2번 호출되었습니다.3.6 functools.wraps의 중요성
데코레이터를 만들 때는 functools.wraps를 사용해야 원본 함수의 메타데이터(이름, 독스트링 등)가 보존된다:
from functools import wraps
def my_decorator(func):
@wraps(func) # 이것이 중요!
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def example_function():
"""이것은 예제 함수입니다."""
pass
print(example_function.__name__) # 'example_function' (wraps 없으면 'wrapper')
print(example_function.__doc__) # '이것은 예제 함수입니다.' (wraps 없으면 None)3.7 dataclass 데코레이터
- @dataclass는 Python에서 클래스를 간단하고 명확하게 정의할 수 있도록 도와주는 데코레이터
- @dataclass를 붙이면 Python이 아래와 같은 기본 메서드들을 자동으로 정의
| 메서드 이름 | 기능 |
|---|---|
__init__ |
생성자. 필드 값을 자동으로 받아서 객체 생성 |
__repr__ |
객체 출력 시 보기 좋게 문자열로 표현 |
__eq__ |
두 객체의 값이 같으면 같은 객체로 취급 |
__hash__ |
객체를 hashable 하게 만듦 (옵션) |
- 일반 클래스 without @dataclass
class RuleViolation:
def __init__(self, rule_number, rule_name, severity, description, token, physical_abbr, expected_abbr):
self.rule_number = rule_number
self.rule_name = rule_name
self.severity = severity
self.description = description
self.token = token
self.physical_abbr = physical_abbr
self.expected_abbr = expected_abbr
def __repr__(self):
return f"RuleViolation(
{self.rule_number},
{self.rule_name},
{self.severity},
{self.description},
{self.token},
{self.physical_abbr},
{self.expected_abbr})"
rule_violation = RuleViolation(
rule_number="R123",
rule_name="Rule Name",
severity="ERROR",
description="Description of the rule violation",
token="TOKEN",
physical_abbr="PHYS",
expected_abbr="EXP")
print(rule_violation)
# RuleViolation(R123, Rule Name, ERROR, Description of the rule violation, TOKEN, PHYS, EXP)- @dataclass 사용
- 일반 클래스 with @dataclass 보다 더 간결하고 명확하게 표현할 수 있다.
from dataclasses import dataclass
@dataclass
class RuleViolation:
rule_number: str
rule_name: str
severity: str
description: str
token: str
physical_abbr: str
expected_abbr: str
rule_violation = RuleViolation(
rule_number="R123",
rule_name="Rule Name",
severity="ERROR",
description="Description of the rule violation",
token="TOKEN",
physical_abbr="PHYS",
expected_abbr="EXP")
print(rule_violation)
# RuleViolation(R123, Rule Name, ERROR, Description of the rule violation, TOKEN, PHYS, EXP)