Python Decorator

Enhancing Functions with Decorators

파이썬 데코레이터(Decorator)는 함수나 클래스를 수정하지 않고도 기능을 추가하거나 확장할 수 있게 해주는 강력한 문법이다. @기호를 사용해 함수 위에 선언하며, 로깅, 인증, 성능 측정, 캐싱 등의 공통 기능을 효과적으로 구현할 수 있다. 데코레이터는 함수를 인자로 받아 새로운 함수를 반환하는 고차 함수(Higher-order function)의 개념을 바탕으로 한다.

Engineering
Python
저자

Kwangmin Kim

공개

2023년 07월 01일

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 데코레이터 예제

def simple_decorator(func):
    def wrapper():
        print("함수 실행 전")
        # 여기에 감싸기 전후 작업 삽입
        func()
        print("함수 실행 후")
    return wrapper

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

say_hello()
# 출력:
# 함수 실행 전
# Hello!
# 함수 실행 후

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. 코드 재사용성: 공통 기능을 여러 함수에 쉽게 적용할 수 있다.
  2. 관심사 분리: 비즈니스 로직과 부가 기능을 분리할 수 있다.
  3. 가독성: 함수의 핵심 로직에 집중할 수 있다.
  4. 유지보수성: 공통 기능의 수정이 용이하다.

1.6 데코레이터 사용 시 주의사항

  1. 성능: 데코레이터는 함수 호출에 오버헤드를 추가한다.
  2. 디버깅: 데코레이터가 적용된 함수는 디버깅이 복잡할 수 있다.
  3. 메타데이터: 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.0

3.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)  

Subscribe

Enjoy this blog? Get notified of new posts by email: