38. 데코레이터: 함수에 동작 추가하기
데코레이터(decorator)는 깔끔하고 재사용 가능한 코드를 작성하기 위한 Python의 가장 강력한 기능 중 하나입니다. 데코레이터를 사용하면 함수의 실제 코드를 바꾸지 않고도 함수의 동작을 수정하거나 확장할 수 있습니다. 이 장에서는 23장에서 배운 일급 함수(first-class function)와 클로저(closure)에 대한 이해를 바탕으로, 데코레이터가 어떻게 동작하며 효과적으로 사용하는 방법을 살펴봅니다.
38.1) 데코레이터란 무엇이며 왜 유용한가
데코레이터는 다른 함수를 입력으로 받아 수정된 버전의 함수를 반환하는 함수입니다. 이것이 가능한 이유는 23장에서 배운 것처럼, Python의 함수는 일급 객체(first-class objects)이기 때문입니다—함수는 인자로 전달되고 다른 함수에서 반환될 수 있습니다. 데코레이터는 기존 함수 주위에 추가 동작을 "감싸서", 핵심 로직을 복잡하게 만들지 않고 로깅, 타이밍, 검증, 접근 제어와 같은 공통 기능을 쉽게 추가할 수 있게 합니다.
데코레이터가 중요한 이유
프로그램에 여러 함수가 있고, 각 함수가 호출될 때 로그를 남기고 싶다고 상상해봅시다. 데코레이터 없이는 다음과 같이 작성할 수 있습니다:
# 데코레이터 없이 - 중복된 로깅 코드
def calculate_total(prices):
print("Calling calculate_total")
result = sum(prices)
print(f"calculate_total returned: {result}")
return result
def find_average(numbers):
print("Calling find_average")
result = sum(numbers) / len(numbers)
print(f"find_average returned: {result}")
return result
def process_order(order_id):
print("Calling process_order")
result = f"Order {order_id} processed"
print(f"process_order returned: {result}")
return result
# 함수 사용
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60이 접근 방식에는 몇 가지 문제가 있습니다:
- 코드 중복: 로깅 라인이 모든 함수에서 반복됩니다
- 관심사 혼합: 로깅 코드가 비즈니스 로직과 섞여 있습니다
- 유지보수 어려움: 로깅 형식을 변경하려면 모든 함수를 업데이트해야 합니다
- 잊어버리기 쉬움: 새 함수에 로깅을 포함하지 않을 수 있습니다
데코레이터는 로깅 동작을 핵심 함수에서 분리하여 이러한 문제를 해결합니다:
# 데코레이터 사용 - 깔끔하고 유지보수 가능
# (이번 장에서 @log_calls를 만드는 방법을 배웁니다)
@log_calls
def calculate_total(prices):
return sum(prices)
@log_calls
def find_average(numbers):
return sum(numbers) / len(numbers)
@log_calls
def process_order(order_id):
return f"Order {order_id} processed"
# 함수를 사용하면 같은 출력 생성
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60차이점은? 로깅 동작이 @log_calls 데코레이터에 한 번 정의되고 어디서나 재사용됩니다. 핵심 함수는 깔끔하게 유지되며 주요 목적에 집중합니다.
데코레이터의 일반적인 사용 사례
데코레이터는 다음과 같은 경우에 특히 유용합니다:
- 로깅: 함수가 호출되는 시점과 반환하는 값 기록
- 타이밍: 함수 실행에 걸리는 시간 측정
- 검증: 함수 인자가 특정 요구사항을 충족하는지 확인
- 캐싱: 비용이 많이 드는 함수 호출 결과를 재사용을 위해 저장
- 접근 제어: 함수 실행을 허용하기 전에 권한 확인
- 재시도 로직: 실패한 작업 자동 재시도
- 타입 검사: 인자와 반환 타입 검증
핵심 장점은 데코레이터를 한 번 작성하면 단일 코드 라인으로 많은 함수에 적용할 수 있다는 것입니다.
38.2) 객체로서의 함수: 데코레이터의 기반
데코레이터를 이해하기 전에, Python에서 함수(function)가 일급 객체라는 개념을 복습하고 확장할 필요가 있습니다. 23장에서 배웠듯이, 이는 함수가 변수에 할당될 수 있고, 인수로 전달될 수 있으며, 다른 함수에서 반환될 수도 있다는 뜻입니다.
함수는 변수에 할당할 수 있습니다
함수를 정의하면 Python은 함수 객체를 만들고 그 객체를 이름에 바인딩(bind)합니다:
def greet(name):
return f"Hello, {name}!"
# 함수 객체는 다른 변수에 할당할 수 있습니다
say_hello = greet
# 두 이름은 같은 함수 객체를 가리킵니다
print(greet("Alice")) # Output: Hello, Alice!
print(say_hello("Bob")) # Output: Hello, Bob!이름 greet와 say_hello는 둘 다 같은 함수 객체를 가리킵니다. 이는 데코레이터가 동작하는 방식의 핵심입니다.
함수는 인수로 전달할 수 있습니다
함수는 다른 값과 마찬가지로 다른 함수에 전달할 수 있습니다:
def apply_twice(func, value):
"""값에 함수를 두 번 적용합니다."""
result = func(value)
result = func(result)
return result
def add_five(x):
return x + 5
result = apply_twice(add_five, 10)
print(result) # Output: 20 (10 + 5 = 15, then 15 + 5 = 20)여기서 apply_twice는 add_five 함수를 인수로 받아 두 번 호출합니다.
함수는 다른 함수를 반환할 수 있습니다
함수는 새 함수를 만들어 반환할 수 있습니다:
def make_multiplier(factor):
"""특정 factor로 곱하는 함수를 생성합니다."""
def multiply(x):
return x * factor
return multiply
times_three = make_multiplier(3)
times_five = make_multiplier(5)
print(times_three(10)) # Output: 30
print(times_five(10)) # Output: 50make_multiplier 함수는 클로저(23장에서 배운 내용)를 통해 factor 값을 “기억하는” 새 함수를 반환합니다.
함수 래핑: 핵심 데코레이터 패턴
데코레이터 패턴은 이러한 개념들을 결합합니다: 함수를 입력으로 받아 동작을 추가하는 래퍼(wrapper) 함수를 생성하고 래퍼를 반환하는 함수:
def simple_wrapper(original_func):
"""Wrap a function with additional behavior."""
def wrapper():
print("Before calling the function")
result = original_func()
print("After calling the function")
return result
return wrapper
def say_hello():
print("Hello!")
return "greeting"
# 수동으로 함수 래핑
wrapped_hello = simple_wrapper(say_hello)
return_value = wrapped_hello()
# Output:
# Before calling the function
# Hello!
# After calling the function
print(f"Returned: {return_value}")
# Output: Returned: greeting무슨 일이 일어나는지 추적해봅시다:
simple_wrapper가say_hello를original_func로 받습니다- 새로운 함수
wrapper를 생성합니다:- "Before calling the function" 출력
original_func()호출 (say_hello입니다)- "After calling the function" 출력
- 결과 반환
simple_wrapper가wrapper함수를 반환합니다wrapped_hello()를 호출하면 실제로wrapper를 호출하며, 그 안에서 원본say_hello를 호출합니다
이것이 모든 데코레이터의 핵심 패턴입니다.
인자가 있는 함수 처리하기
위의 래퍼는 인자를 받지 않는 함수에만 작동합니다. 모든 함수와 작동하게 하려면 *args와 **kwargs가 필요합니다:
def flexible_wrapper(original_func):
"""Wrap a function that can accept any arguments."""
def wrapper(*args, **kwargs):
# *args는 위치 인자 캡처
# **kwargs는 키워드 인자 캡처
print("Before calling the function")
result = original_func(*args, **kwargs)
print("After calling the function")
return result
return wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
# 수동으로 함수 래핑
greet = flexible_wrapper(greet)
result = greet("Alice")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hello, Alice!
result = greet("Bob", greeting="Hi")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hi, Bob!*args와 **kwargs 작동 방식:
20장에서 배운 것처럼, *args와 **kwargs는 함수가 가변 개수의 인자를 받을 수 있게 합니다:
*args는 모든 위치 인자를 튜플로 수집**kwargs는 모든 키워드 인자를 딕셔너리로 수집original_func(*args, **kwargs)를 호출할 때 원본 함수의 인자로 다시 언패킹
이 패턴은 래퍼가 인자 개수와 관계없이 모든 함수와 작동하게 합니다.
더 깔끔한 문법으로
이 패턴이 데코레이터의 기초입니다. 다음에 배울 데코레이터 문법은 이 패턴을 적용하는 더 깔끔한 방법일 뿐입니다. 다음과 같이 작성하는 대신:
greet = flexible_wrapper(greet)@ 문법을 사용합니다:
@flexible_wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"둘 다 정확히 같은 일을 합니다—@ 문법은 코드를 더 깔끔하고 읽기 쉽게 만드는 문법적 설탕(syntactic sugar)일 뿐입니다.
38.3) @decorator 문법: 더 깔끔한 적용
function_name = decorator(function_name) 형태는 동작하지만 장황하고 빼먹기 쉽습니다. Python은 데코레이터를 더 깔끔하게 적용할 수 있도록 @decorator 문법을 제공합니다.
@ 기호 사용하기
함수를 수동으로 감싸는 대신, 함수 정의 바로 위 줄에 @decorator_name을 놓을 수 있습니다:
def log_call(func):
"""함수 호출을 로깅하는 데코레이터."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def calculate_total(prices):
return sum(prices)
@log_call
def find_average(numbers):
return sum(numbers) / len(numbers)
# 데코레이트된 함수 사용
total = calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = find_average([10, 20, 30])
# Output:
# Calling find_average
# find_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0@log_call 문법은 다음과 완전히 동일합니다:
def calculate_total(prices):
return sum(prices)
calculate_total = log_call(calculate_total)하지만 @ 문법이 훨씬 깔끔하고, 해당 함수가 데코레이트되었다는 사실이 즉시 드러납니다.
여러 데코레이터 쌓기
데코레이터는 여러 개를 같은 함수에 쌓아서 적용할 수 있습니다:
import time
def log_call(func):
"""함수 호출을 로깅하는 데코레이터."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
def timer(func):
"""함수 실행 시간을 측정하는 데코레이터."""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start_time
print(f"{func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timer
@log_call
def process_data(items):
total = sum(items)
return total * 2
result = process_data([1, 2, 3, 4, 5])
# Output:
# Calling process_data
# process_data returned: 30
# process_data took 0.0001 seconds
print(f"Final result: {result}")
# Output: Final result: 30데코레이터가 쌓이면 아래에서 위로 (함수에 가장 가까운 것부터) 적용됩니다:
@timer # 두 번째 적용 (가장 바깥쪽 레이어)
@log_call # 첫 번째 적용 (함수에 가장 가까움)
def process_data(items):
pass이것은 다음과 같습니다:
process_data = timer(log_call(process_data))적용 순서 (아래에서 위로):
@log_call이 원본 함수를 먼저 래핑@timer가 결과를 래핑 (이미 래핑된 함수를 래핑)
실행 순서 (위에서 아래로, 가장 바깥쪽에서 안쪽으로):
timerwrapper 시작 (가장 바깥쪽, 먼저 실행)log_callwrapper 시작 (안쪽 wrapper)- 원본 함수 실행
log_callwrapper 종료timerwrapper 종료 (가장 바깥쪽, 마지막 종료)
데코레이터를 포장지 층처럼 생각하세요—안쪽에서 바깥쪽으로 적용하지만, 포장을 풀 때(실행)는 바깥쪽에서 안쪽으로 갑니다.
데코레이터 적용:
실행 흐름:
38.4) 실용적인 데코레이터 예제 (로깅, 타이밍, 검증)
이제 실제 프로그램에서 사용할 만한 여러 데코레이터를 살펴봅시다. 이 예제들은 흔한 패턴을 보여 주고, 데코레이터가 현실 문제를 어떻게 해결하는지 보여 줍니다.
예제 1: 향상된 로깅 데코레이터
타임스탬프를 포함하고 예외를 처리하는, 더 정교한 로깅 데코레이터입니다:
import time
def log_with_timestamp(func):
"""타임스탬프와 함께 함수 호출을 로깅하는 데코레이터."""
def wrapper(*args, **kwargs):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] Calling {func.__name__}")
try:
result = func(*args, **kwargs)
print(f"[{timestamp}] {func.__name__} completed successfully")
return result
except Exception as e:
print(f"[{timestamp}] {func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
@log_with_timestamp
def divide(a, b):
return a / b
@log_with_timestamp
def process_user(user_id):
# 처리 과정을 시뮬레이션
if user_id < 0:
raise ValueError("User ID must be positive")
return f"Processed user {user_id}"
# 성공적인 실행 테스트
result = divide(10, 2)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide completed successfully
print(f"Result: {result}")
# Output: Result: 5.0
# 검증을 포함한 성공적인 실행 테스트
user = process_user(42)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user completed successfully
print(user)
# Output: Processed user 42
# 예외 처리 테스트
try:
divide(10, 0)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide raised ZeroDivisionError: division by zero
except ZeroDivisionError:
print("Handled division by zero")
# Output: Handled division by zero
try:
process_user(-5)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user raised ValueError: User ID must be positive
except ValueError:
print("Handled invalid user ID")
# Output: Handled invalid user ID이 데코레이터는:
- 모든 로그 메시지에 타임스탬프를 추가합니다
- 성공적인 완료와 예외 모두를 로깅합니다
- 예외를 로깅한 뒤 다시 발생시킵니다(인수 없이
raise사용) try/except블록으로 어떤 예외든 잡아 로깅합니다
예제 2: 성능 타이밍 데코레이터
함수 실행 시간을 측정하고 보고하는 데코레이터입니다:
import time
def measure_time(func):
"""실행 시간을 측정하고 보고하는 데코레이터."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
# 시간을 적절히 포맷팅
if elapsed < 0.001:
time_str = f"{elapsed * 1000000:.2f} microseconds"
elif elapsed < 1:
time_str = f"{elapsed * 1000:.2f} milliseconds"
else:
time_str = f"{elapsed:.2f} seconds"
print(f"{func.__name__} executed in {time_str}")
return result
return wrapper
@measure_time
def find_primes(limit):
"""limit까지의 모든 소수를 찾습니다."""
primes = []
for num in range(2, limit):
is_prime = True
for divisor in range(2, int(num ** 0.5) + 1):
if num % divisor == 0:
is_prime = False
break
if is_prime:
primes.append(num)
return primes
@measure_time
def calculate_factorial(n):
"""n의 팩토리얼을 계산합니다."""
result = 1
for i in range(1, n + 1):
result *= i
return result
# 데코레이트된 함수 테스트
primes = find_primes(1000)
# Output: find_primes executed in 15.23 milliseconds
print(f"Found {len(primes)} primes")
# Output: Found 168 primes
factorial = calculate_factorial(100)
# Output: calculate_factorial executed in 45.67 microseconds
print(f"Factorial has {len(str(factorial))} digits")
# Output: Factorial has 158 digits이 데코레이터는 지속 시간에 따라(마이크로초, 밀리초, 초) 시간 측정값을 자동으로 적절히 포맷팅합니다.
예제 3: 입력 검증 데코레이터
실행 전에 함수 인수를 검증하는 데코레이터입니다:
def validate_positive(func):
"""모든 숫자 인수가 양수인지 보장하는 데코레이터."""
def wrapper(*args, **kwargs):
# 위치 인수 확인
for i, arg in enumerate(args):
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(
f"Argument {i} to {func.__name__} must be positive, got {arg}"
)
# 키워드 인수 확인
for key, value in kwargs.items():
if isinstance(value, (int, float)) and value <= 0:
raise ValueError(
f"Argument '{key}' to {func.__name__} must be positive, got {value}"
)
return func(*args, **kwargs)
return wrapper
@validate_positive
def calculate_area(width, height):
"""직사각형의 넓이를 계산합니다."""
return width * height
@validate_positive
def calculate_discount(price, discount_percent):
"""할인된 가격을 계산합니다."""
discount = price * (discount_percent / 100)
return price - discount
# 유효한 입력 테스트
area = calculate_area(10, 5)
print(f"Area: {area}")
# Output: Area: 50
discounted = calculate_discount(100, 20)
print(f"Discounted price: ${discounted:.2f}")
# Output: Discounted price: $80.00
# 유효하지 않은 입력 테스트
try:
calculate_area(-5, 10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 0 to calculate_area must be positive, got -5
try:
calculate_discount(100, discount_percent=-10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 'discount_percent' to calculate_discount must be positive, got -10이 데코레이터는:
- 모든 숫자 인수(위치 인수와 키워드 인수 모두)를 확인합니다
- 양수가 아닌 값이 있으면 설명적인 에러를 발생시킵니다
- 어떤 인수가 검증에 실패했는지 알려주는 명확한 에러 메시지를 제공합니다
38.5) (선택) 인자를 받는 데코레이터
지금까지 우리의 모든 데코레이터는 함수를 입력으로 받는 단순한 함수였습니다. 그러나 데코레이터의 동작을 설정하고 싶다면 어떻게 할까요? 예를 들어, 시도 횟수를 지정할 수 있는 재시도 데코레이터나 로그 레벨을 지정할 수 있는 로깅 데코레이터를 원할 수 있습니다.
인자를 받는 데코레이터는 추가적인 함수 중첩 레벨이 필요합니다. 데코레이터가 함수를 받는 함수가 아니라, 인자를 받아 데코레이터를 반환하는 함수가 됩니다.
패턴: 데코레이터 팩토리
인자를 받는 데코레이터는 실제로 데코레이터 팩토리입니다 - 데코레이터를 생성하고 반환하는 함수입니다. 이를 이해하는 핵심은 Python이 @ 기호로 무엇을 하는지 아는 것입니다.
핵심 원칙: Python은 @ 를 먼저 평가함
Python은 항상 @ 뒤에 오는 것을 먼저 평가한 다음, 그 결과를 사용하여 함수를 장식합니다.
비교해봅시다:
A) 기본 데코레이터:
다음 예제를 기반으로 설명합니다:
def log_call(func):
"""함수 호출을 로깅하는 데코레이터."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def greet(name):
return f"Hello, {name}!"Python이 하는 일:
@log_call평가 → 결과:log_call자체 (함수 객체)greet에 적용:greet = log_call(greet)
B) 데코레이터 팩토리:
다음 예제를 기반으로 설명합니다:
def repeat(times):
"""Level 1: Factory - 설정 받음"""
def decorator(func):
"""Level 2: Decorator - 장식할 함수 받음"""
def wrapper(*args, **kwargs):
"""Level 3: Wrapper - 장식된 함수 호출 시 실행"""
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!Python이 하는 일:
@repeat(3)평가 → 결과:repeat(3)이 호출됨, decorator 함수 반환- 그 decorator를
greet에 적용:greet = decorator(greet)
차이점: @log_call은 함수 자체를 주지만, @repeat(3)은 decorator를 반환하는 함수(repeat)를 호출합니다.
3단계 이해하기
데코레이터 팩토리는 3개의 중첩 함수를 가지며, 각각 특정 역할이 있습니다:
def repeat(times): # Level 1: Factory
def decorator(func): # Level 2: Decorator
def wrapper(*args, **kwargs): # Level 3: Wrapper
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decoratorLevel 1 - Factory (repeat):
- 받음: 설정 (
times) - 반환: Decorator 함수
- 호출 시점: Python이
@repeat(3)평가할 때
Level 2 - Decorator (decorator):
- 받음: 장식할 함수 (
func) - 반환: Wrapper 함수
- 호출 시점: Level 1 직후, @ 문법의 일부로
Level 3 - Wrapper (wrapper):
- 받음: 호출 시 함수의 인자 (
*args, **kwargs) - 반환: 결과
- 호출 시점: 장식된 함수를 호출할 때마다
단계별 실행
@repeat(3)에서 무슨 일이 일어나는지 추적해봅시다:
# 작성한 코드:
@repeat(3)
def greet(name):
print(f"Hello, {name}!")1단계: Python이 repeat(3) 평가
decorator = repeat(3) # Factory가 decorator 반환 (times=3 캡처됨)2단계: Python이 decorator를 greet에 적용
def greet(name):
print(f"Hello, {name}!")
greet = decorator(greet) # Decorator가 wrapper 반환 (func=greet 캡처됨)참고: 이 시점에서 greet은 이제 wrapper 함수를 가리킵니다. 원본 greet은 func에 캡처되어 있습니다.
3단계: greet("Alice") 호출 시, wrapper 실행
greet("Alice") # 실제로는 wrapper("Alice") 호출
# wrapper는 캡처된 'times'와 'func' 사용왜 3단계인가?
각 레벨은 클로저를 통해 다른 정보를 캡처합니다:
def repeat(times): # 캡처: times
def decorator(func): # 캡처: func (그리고 times 기억)
def wrapper(*args, **kwargs): # 캡처: times, func, 그리고 args 받음
for i in range(times): # 캡처된 'times' 사용
result = func(*args, **kwargs) # 캡처된 'func'과 'args' 사용
return result
return wrapper
return decorator- Level 1은 설정 캡처 (
times) - Level 2는 장식할 함수 캡처 (
func) - Level 3은 호출 시 인자 받음 (
args,kwargs)
3단계 모두 없이는 설정과 장식하는 함수 모두를 기억하는 설정 가능한 데코레이터를 가질 수 없습니다.
예제 1: 설정 가능한 로깅 데코레이터
설정을 받을 수 있는 로깅 데코레이터의 실용적인 예시입니다:
def log_with_prefix(prefix="LOG"):
"""사용자 지정 prefix로 로깅 데코레이터를 만드는 데코레이터 팩토리."""
def decorator(func):
def wrapper(*args, **kwargs):
print(f"[{prefix}] Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"[{prefix}] {func.__name__} returned: {result}")
return result
return wrapper
return decorator
@log_with_prefix(prefix="INFO")
def calculate_total(prices):
return sum(prices)
@log_with_prefix() # 기본 prefix 사용
def get_average(numbers):
return sum(numbers) / len(numbers)
# 데코레이트된 함수 테스트
total = calculate_total([10, 20, 30])
# Output:
# [INFO] Calling calculate_total
# [INFO] calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = get_average([10, 20, 30])
# Output:
# [LOG] Calling get_average
# [LOG] get_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0다음 사항에 주목하세요:
@log_with_prefix(prefix="INFO")는 사용자 지정 prefix를 사용합니다@log_with_prefix()는 기본 prefix "LOG"를 사용합니다- 기본값을 사용하더라도 괄호는 반드시 포함해야 합니다
예제 2: 여러 인수를 받는 데코레이터
숫자 범위를 검증하는 데코레이터입니다:
def validate_range(min_value=None, max_value=None):
"""
숫자 인수가 범위 내에 있는지 검증하는 데코레이터 팩토리.
Args:
min_value: 허용되는 최솟값(포함)
max_value: 허용되는 최댓값(포함)
"""
def decorator(func):
def wrapper(*args, **kwargs):
# 모든 숫자 인수 확인
all_args = list(args) + list(kwargs.values())
for arg in all_args:
if isinstance(arg, (int, float)):
if min_value is not None and arg < min_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is below minimum {min_value}"
)
if max_value is not None and arg > max_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is above maximum {max_value}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_range(min_value=0, max_value=100)
def calculate_percentage(value, total):
"""퍼센트를 계산합니다."""
return (value / total) * 100
@validate_range(min_value=0)
def calculate_age(birth_year, current_year):
"""출생 연도에서 나이를 계산합니다."""
return current_year - birth_year
# 유효한 입력 테스트
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")
# Output: Percentage: 25.0%
age = calculate_age(1990, 2025)
print(f"Age: {age}")
# Output: Age: 35
# 유효하지 않은 입력 테스트
try:
calculate_percentage(150, 100)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_percentage received 150, which is above maximum 100
try:
calculate_age(-5, 2025)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_age received -5, which is below minimum 0인수를 받는 데코레이터를 언제 사용할까
다음과 같은 경우에 인수를 받는 데코레이터를 사용합니다:
- 데코레이터의 동작을 설정해야 할 때
- 같은 데코레이터가 문맥에 따라 다르게 동작해야 할 때
- 데코레이터를 더 재사용 가능하고 유연하게 만들고 싶을 때
일반적인 예시는 다음과 같습니다:
- 시도 횟수와 지연 시간을 설정할 수 있는 재시도 데코레이터
- 로그 레벨이나 포맷을 설정할 수 있는 로깅 데코레이터
- 규칙을 설정할 수 있는 검증 데코레이터
- 캐시 크기나 만료 시간을 설정할 수 있는 캐싱 데코레이터
- 제한을 설정할 수 있는 레이트 리미팅(rate limiting) 데코레이터
복잡성에 대한 참고
인수를 받는 데코레이터는 복잡성이 한 단계 늘어납니다. 이를 작성할 때는:
- 명확하고 설명적인 매개변수 이름을 사용하고
- 합리적인 기본값을 제공하며
- 매개변수를 설명하는 docstring을 포함하고
- 추가된 유연성이 복잡성을 감수할 만큼 가치가 있는지 고려하세요
간단한 경우에는 인수 없는 데코레이터가 더 명확하고 이해하기 쉬운 경우가 많습니다.
데코레이터는 깔끔하고 유지보수 가능한 Python 코드를 작성하기 위한 강력한 도구입니다. 데코레이터는 로깅(logging), 타이밍(timing), 검증(validation) 같은 횡단 관심사(cross-cutting concern)를 핵심 비즈니스 로직에서 분리할 수 있게 해 주어, 코드를 더 읽기 쉽고 테스트하기 쉽고 수정하기 쉽게 만듭니다. Python으로 계속 프로그래밍하다 보면 프레임워크(framework)와 라이브러리(library)에서 데코레이터가 광범위하게 사용되는 것을 보게 될 것이고, 흔한 문제를 우아하게 해결하기 위해 여러분만의 데코레이터를 작성할 기회도 많이 발견하게 될 것입니다.