파이썬 데코레이터 패턴: 복사하여 영원히 사용할 가치가 있는 패턴들

Custom Python Decorator Patterns Worth Copy-Pasting Forever

파이썬을 어느 정도 사용해보셨다면, 데코레이터를 보고 사용해 보셨을 것입니다. 많은 개발자들이 데코레이터의 기본 개념을 이해하고 있지만, 유용하고 재사용 가능한 패턴 모음을 갖추면 코드 품질과 생산성을 향상시킬 수 있습니다.

이 글에서는 여러분의 도구 모음에 추가할 가치가 있는 다섯 가지 데코레이터 패턴을 살펴보겠습니다. 각 패턴마다 샘플 구현과 예제를 함께 제공합니다.

데코레이터란 무엇인가?

본격적으로 시작하기 전에, 데코레이터가 무엇인지 간단히 살펴보겠습니다. 핵심적으로, 데코레이터는 다른 함수(또는 클래스)의 소스 코드를 변경하지 않고 그 동작을 수정하는 함수입니다. 다음과 같은 간단한 패턴을 따릅니다:

@decorator

def function():

pass

이는 다음 구문의 간편한 표현입니다:

function = decorator(function)

이제 본격적인 내용으로 들어가 봅시다.

1. 메모이제이션(Memoization)

입력에 따라 계산하는데 상당한 시간이 걸리는 함수가 있다고 가정해 보겠습니다. 예를 들어, 복잡한 수학 계산, 대규모 데이터셋에 대한 집약적인 작업, API 호출 등이 있습니다. 매번 다시 계산하는 것은 낭비입니다.

다음은 간단한 메모이제이션 데코레이터입니다:

def memoize(func):

"""Caches the return value of a function based on its arguments."""

cache = {}

def wrapper(args, *kwargs):

# Create a key that uniquely identifies the function call

key = str(args) + str(kwargs)

if key not in cache:

cache[key] = func(args, *kwargs)

return cache[key]

return wrapper

예제: 피보나치 수열

재귀적으로 피보나치 수를 계산하는 함수가 있다고 가정해 봅시다. 다음과 같이 메모이제이션을 적용할 수 있습니다:

@memoize

def fibonacci(n):

"""Calculate the nth Fibonacci number."""

if n <= 1:

return n

return fibonacci(n-1) + fibonacci(n-2)

Without memoization, this would be slow

result = fibonacci(50)

print(f"The 50th Fibonacci number is {result}")

출력:

The 50th Fibonacci number is 12586269025

메모이제이션 없이는 재귀 호출로 인해 fibonacci(50)을 계산하는 데 시간이 오래 걸립니다. 메모이제이션을 사용하면 거의 즉시 계산됩니다.

사용 시기: 동일한 입력을 재처리하고 함수 결과가 시간이 지나도 변하지 않을 때 이 패턴을 사용하세요.

2. 로깅(Logging)

함수 내부에 print 문이나 다른 디버그 라인을 흩뿌리고 싶지 않을 때가 있습니다. 특히 여러 함수에 걸쳐 일관된 로그를 원할 때 유용합니다.

이 데코레이터는 함수 호출에 대한 정보를 로깅하며, 디버깅 및 모니터링에 유용합니다:

import logging

import functools

def log_calls(func=None, level=logging.INFO):

"""Log function calls with arguments and return values."""

def decorator(func):

@functools.wraps(func)

def wrapper(args, *kwargs):

args_str = ", ".join([str(a) for a in args])

kwargs_str = ", ".join([f"{k}={v}" for k, v in kwargs.items()])

allargs = f"{argsstr}{', ' if argsstr and kwargsstr else ''}{kwargs_str}"

logging.log(level, f"Calling {func.name}({all_args})")

result = func(args, *kwargs)

logging.log(level, f"{func.name} returned {result}")

return result

return wrapper

# Handle both @logcalls and @logcalls(level=logging.DEBUG)

if func is None:

return decorator

return decorator(func)

예제: 함수 호출 로깅

다음과 같이 데코레이터를 사용할 수 있습니다:

logging.basicConfig(level=logging.INFO)

@log_calls

def divide(a, b):

return a / b

This will log the call and the return value

result = divide(10, 2)

You can also customize the logging level

@log_calls(level=logging.DEBUG)

def multiply(a, b):

return a * b

result = multiply(5, 4)

사용 시기: 작은 스크립트, API 엔드포인트 또는 배치 작업 디버깅에 편리합니다.

3. 실행 시간 측정

때로는 함수 실행 시간에 대한 간단한 벤치마크가 필요할 수 있습니다. 이는 데이터베이스에 접근하거나, 파일을 파싱하거나, 모델을 훈련시키는 코드에 특히 유용합니다.

import time

import functools

def timeit(func):

"""Measure and print the execution time of a function."""

@functools.wraps(func)

def wrapper(args, *kwargs):

start_time = time.time()

result = func(args, *kwargs)

end_time = time.time()

print(f"{func.name} took {endtime - starttime:.4f} seconds to run")

return result

return wrapper

예제: 함수 시간 측정

샘플 함수 호출 시간을 측정해 보겠습니다:

@timeit

def slow_function():

"""A deliberately slow function for demonstration."""

total = 0

for i in range(10000000):

total += i

return total

result = slow_function() # Will print execution time

출력:

slow_function took 0.5370 seconds to run

사용 시기: 이러한 데코레이터는 최적화 범위를 식별하기 위해 간단한 함수의 시간을 측정하는 데 유용합니다.

4. 실패 시 재시도

외부 서비스나 불안정한 작업을 다룰 때, 실패 시 재시도하는 것은 코드를 더 견고하게 만들어 줍니다.

def retry(maxattempts=3, delayseconds=1, backoff_factor=2, exceptions=(Exception,)):

"""Retry a function if it raises specified exceptions."""

def decorator(func):

@functools.wraps(func)

def wrapper(args, *kwargs):

attempts = 0

currentdelay = delayseconds

while attempts < max_attempts:

try:

return func(args, *kwargs)

except exceptions as e:

attempts += 1

if attempts == max_attempts:

logging.error(f"Failed after {attempts} attempts. Last error: {e}")

raise

logging.warning(

f"Attempt {attempts} failed with error: {e}. "

f"Retrying in {current_delay} seconds..."

)

time.sleep(current_delay)

currentdelay *= backofffactor

return wrapper

return decorator

어떻게 작동하나요? 이 재시도 데코레이터는 지정된 예외가 발생할 때 최대 시도 횟수까지 함수를 자동으로 재시도합니다. 각 시도 후 재시도 간 지연 시간이 구성 가능한 요소만큼 증가하는 지수 백오프 전략을 구현합니다.

이 데코레이터는 최대 시도 횟수, 초기 지연 시간, 백오프 승수, 그리고 어떤 예외 유형을 포착할지 등 동작을 사용자 정의할 수 있는 매개변수를 허용합니다. 재시도 중에는 경고를 로깅하고, 모든 시도가 실패하면 최종 오류를 로깅한 후 마지막 예외를 다시 발생시킵니다.

대상 함수를 감싸는 동안 재시도 구성을 유지하는 클로저를 만들기 위해 중첩 함수를 사용합니다.

예제: 재시도를 통한 API 쿼리

다음은 재시도 로직을 사용하여 API에서 데이터를 가져오는 예제입니다:

import random

import requests

@retry(maxattempts=5, delayseconds=1, exceptions=(requests.RequestException,))

def fetch_data(url):

"""Fetch data from an API with retry logic."""

response = requests.get(url, timeout=2)

response.raiseforstatus() # Raise exception for 4XX/5XX responses

return response.json()

This will retry up to 5 times if the request fails

try:

data = fetch_data('https://api.example.com/data')

print("Successfully fetched data!")

except Exception as e:

print(f"All retry attempts failed: {e}")

사용 시기: 네트워크 코드, 불안정한 API 또는 I/O 바인딩과 관련된 모든 것에 적합합니다.

5. 입력 유효성 검사

함수 자체를 복잡하게 만들지 않고도 입력의 유효성을 자동으로 검사하는 래퍼를 만들 수 있습니다.

다음은 입력이 양의 정수인지 확인하는 데코레이터입니다:

def validatepositiveints(func):

def wrapper(*args):

for arg in args:

if not isinstance(arg, int) or arg <= 0:

raise ValueError(f"{arg} must be a positive integer")

return func(*args)

return wrapper

예제: 양수 입력 유효성 검사

함수가 실행되기 전에 인수를 반복하여 유효성을 검사합니다:

@validatepositiveints

def calculate_area(length, width):

return length * width

print(calculate_area(5, 10))

print(calculate_area(-1, 10))

첫 번째 호출은 예상대로 50을 반환하지만, 두 번째 함수 호출은 ValueError 예외가 발생합니다.

50

---------------------------------------------------------------------------

ValueError Traceback (most recent call last)

in ()

4

5 print(calculate_area(5, 10))

-----> 6 print(calculate_area(-1, 10))

in wrapper(*args)

3 for arg in args:

4 if not isinstance(arg, int) or arg <= 0:

-----> 5 raise ValueError(f"{arg} must be a positive integer")

6 return func(*args)

7 return wrapper

ValueError: -1 must be a positive integer

사용 시기: 데이터 파이프라인과 사용자 입력을 다룰 때 유용합니다.

요약

데코레이터는 추가 동작을 깔끔하게 "결합"하는 데 도움이 되는 래퍼로 생각할 수 있습니다. 살펴본 다섯 가지 패턴—메모이제이션, 로깅, 타이밍, 재시도 로직, 그리고 입력 유효성 검사—는 흔한 프로그래밍 작업에 대한 가장 좋은 해결책입니다.

이러한 패턴을 프로젝트에 사용하면(또는 더 나아가 작은 유틸리티 모듈을 만드는 것이 좋습니다) 더 깔끔하고 유지 관리하기 쉬운 코드를 작성하고 중복된 노력을 줄일 수 있습니다.

코드의 어떤 측면이 추상화되어 전체 코드베이스에 일관되게 적용될 수 있는지 생각해 보세요. 이런 관점으로 코드를 보기 시작하면 데코레이터의 용도를 더 많이 발견할 수 있을 것입니다.