데코레이터는 다른 함수를 인자로 받아서, 전달받은 함수의 동작을 확장하거나 대체할 목적으로 사용하는 함수예요.
함수 A가 데코레이터이고 함수 B가 그 인자라면, 함수 A는 함수 B의 코드를 수정하지 않고도 함수 B의 동작을 바꾸거나 확장하거나 대체해요.
데코레이터 함수 A가 함수 B를 _감싼다_고 말해요.
"동작을 수정한다"고 말하지만, 감싸진 함수는 실제로 바뀌지 않아요.
동작은 감싸진 함수 주위에 (그리고 그 함수가 반환하는 것에) 추가되거나, 감싸진 함수의 동작이 다른 동작으로 대체될 뿐이에요.
고차 함수는 하나 이상의 함수를 인자로 받거나, 하나 이상의 함수를 반환하는 함수예요. 다른 함수에 인자로 전달되거나 다른 함수에서 반환되는 함수를 일급 함수라고 해요. 파이썬 함수는 호출 가능한 객체로서, 다른 값이나 객체와 마찬가지로 변수에 저장하거나 인자로 쓸 수 있는 일급 함수예요. 고차 함수와 일급 함수가 서로 어우러지기 때문에 데코레이터를 만들 수 있어요.
@ 기호를 데코레이터 함수 이름 앞에 붙이고, 데코레이션할 함수 바로 위에 놓아요. 다음과 같이요:
@decorator
def decorated_function():
pass
일부 데코레이터는 인자를 받아요:
@decorator_with_arg(name="Bob")
def decorated_function2():
pass
데코레이터에 기본 인자가 정의되어 있다면, 다른 함수를 호출할 때와 마찬가지로 데코레이터가 제대로 동작하도록 @decorator() 호출에 괄호를 써야 해요:
@decorator_with_default_arg()
def decorated_function3():
pass
데코레이터가 위치 인자를 받는데 그 인자를 넘기지 않으면, 다음과 비슷한 오류가 발생해요:
TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'
@decorator 문법은 _데코레이션하는 함수_를 호출하면서 _데코레이션되는 함수_를 인자로 넘기는 과정을 짧게 표현한 문법적 설탕이에요.
다음은 데코레이터를 호출하는 다른 방법의 예시예요:
def function():
pass
function = decorator(function)
def function2():
pass
function2 = decorator_with_arg(name="Bob")(function2)
def function3():
pass
function3 = decorator_with_default_arg()(function3)
대부분의 데코레이터는 다른 함수의 동작을 _확장_하거나 _대체_하려는 목적으로 만들어지지만, 어떤 데코레이터는 감싸고 있는 함수를 그대로 반환하기만 하기도 해요.
데코레이터는 최소한 하나의 인자, 즉 자신이 감싸는 함수를 받는 함수예요. 보통은 감싼 함수를 그대로 반환하거나, 감싼 함수를 사용한 표현식의 결과를 반환해요.
감싼 함수를 그대로 반환하기만 하는 간단한 데코레이터는 다음과 같이 작성할 수 있어요:
>>> def do_nothing(func):
... return func
...
... @do_nothing
... def function4():
... return 4
...
>>> print(function4())
4
데코레이터가 로깅에 쓸 추가 정보 같은 부수 효과만 더하기도 해요:
>>> def my_logger(func):
... print(f"Entering {func.__name__}")
... return func
...
... @my_logger
... def my_func():
... print("Hello")
...
>>> my_func()
Entering my_func
Hello
데코레이터는 자기 자신을 반환하지 않아요. 자신의 함수 인자를 반환할 수도 있고, 다른 함수를 반환할 수도 있으며, 전달받거나 데코레이션한 함수의 반환값을 대신하는 하나 이상의 값을 반환할 수도 있어요. 데코레이터가 다른 함수를 반환한다면, 보통은 내부 함수를 반환해요.
함수 안에 함수를 정의할 수 있어요. 이렇게 중첩된 함수를 내부 함수라고 해요. 데코레이터는 내부 함수를 사용해 자신의 함수 인자를 감쌀 수 있어요. 그러면 데코레이터는 자신의 내부 함수를 반환해요. 그다음 내부 함수가 원래의 함수 인자를 반환할 수도 있어요.
다음은 검증에 데코레이터를 사용하는 예시예요:
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
@my_validator
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!
첫 줄에는 데코레이터의 func 인자에 대한 정의가 있어요.
그다음 줄에는 func 인자를 감싸는 데코레이터의 _내부 함수_에 대한 정의가 있어요.
_내부 함수_가 데코레이터의 func 인자를 감싸기 때문에, 내부 함수에는 func에 전달되는 것과 같은 인자가 전달돼요.
래퍼는 func에 정의된 인자와 꼭 같은 이름을 쓸 필요는 없어요.
원래 함수는 planet을 쓰고 데코레이터는 world를 쓰지만, 그래도 데코레이터는 잘 동작해요.
내부 함수는 func를 반환하거나, world == "Pluto"인 경우에는 플루토가 행성이 아니라고 출력해요.
대신 ValueError를 발생시키도록 작성할 수도 있어요.
즉, _내부 함수_는 func를 감싸고, func를 반환하거나 func가 할 일을 대신하는 동작을 해요.
데코레이터는 자신의 _내부 함수_를 반환해요.
_내부 함수_는 전달받은 원래 함수를 반환할 수도 있고 아닐 수도 있어요.
래퍼 함수나 _내부 함수_에서 어떤 코드가 조건부로 실행되는지에 따라, func가 반환될 수도 있고, 오류가 발생할 수도 있으며, func의 반환 타입에 해당하는 값이 반환될 수도 있어요.
*args와 **kwargs 문법을 사용하면 임의 개수의 인자를 받는 함수를 위한 데코레이터를 작성할 수 있어요.
다음은 임의 개수의 인자를 받는 함수를 위한 데코레이터 예시예요:
def double(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 18
print(add(2, 3, 4, 5, 6))
#-> 40
이 데코레이터는 함수 인자의 반환값을 두 배로 만드는 데 잘 동작해요. 반환값을 세 배, 네 배 등으로 만들고 싶다면, 다음 절에서 보여 주는 것처럼 데코레이터 자체에 매개변수를 추가하면 돼요.
다음은 데코레이션된 함수의 반환값에 임의의 값을 곱하도록 설정할 수 있는 데코레이터 예시예요:
def multi(factor=1):
if factor == 0:
raise ValueError("factor must not be 0")
def outer_wrapper(func):
def inner_wrapper(*args, **kwargs):
return func(*args, **kwargs) * factor
return inner_wrapper
return outer_wrapper
@multi(factor=3)
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 27
print(add(2, 3, 4, 5, 6))
#-> 60
처음 몇 줄에서는 factor가 0이 아닌지 검증해요.
그다음 외부 래퍼가 정의돼요.
이 외부 래퍼는 매개변수가 없는 데코레이터에서 기대하는 것과 같은 시그니처를 가져요.
외부 래퍼는 원래 함수와 같은 시그니처를 가진 내부 함수를 가지고 있어요.
내부 래퍼는 원래 함수가 반환한 값에 데코레이터에 전달된 인자를 곱하는 일을 해요.
외부 래퍼는 내부 래퍼를 반환하고, 데코레이터는 외부 래퍼를 반환해요.
다음은 원래 함수에 전달된 인자를 검증할지 여부를 제어하는 매개변수 있는 데코레이터의 예시예요:
def check_for_pluto(check=True):
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if check and world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
return my_validator
@check_for_pluto(check=False)
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!
이렇게 하면 Pluto를 검사할지 여부를 쉽게 바꿀 수 있고, my_func를 수정할 필요도 없어요.