Logo

[파이썬] 데코레이터 기본 사용법

데코레이터(decorator)를 사용하면 함수를 수정하지 않고도 유연하게 함수에 특정 동작을 추가하거나 작동 방식을 바꿀 수 있습니다.

간단한 데코레이터 작성해보기

"Hi!"를 콘솔에 출력하는 say_hi()라는 함수를 작성하고 호출해보겠습니다.

>>> def say_hi():
...     print("Hi!")
...
...
>>> say_hi()
Hi!

다음으로 함수 호출 전/후로 "before""after"를 출력해주는 매우 간단한 데코레이터 함수 decorate()를 작성해보겠습니다.

>>> def decorate(func):
...     def wrapper():
...         print("before")
...         func()
...         print("after")
...     return wrapper

decorate() 함수는 함수를 인자로 받으며, 내부에 wrapper()라는 함수를 선언하고 있습니다. 그리고 wrapper() 함수는 decorate() 함수의 인자로 넘어온 함수를 호출하고 있습니다.

이제 say_hi() 함수를 decorate() 함수로의 인자로 넘긴 후에 다시 say_hi 변수에 재할당해보겠습니다. 파이썬에서 함수는 일반 값처럼 변수에 저장될 수 있고, 다른 함수의 인자로 넘어가거나 리턴값으로 사용될 수 있기 때문에 가능한 일입니다.

>>> say_hi = decorate(say_hi)
>>> say_hi()
before
Hi!
after

자, "Hi!" 출력되기 이 전에 "before"가 출력되고 이 후에 "after"가 출력되는 것을 볼 수 있습니다.

좀 더 깔끔하게 @ 기호와 함께 함수 헤더 위에 바로 데코레이터를 선언하는 방법도 있으며 실무에서는 거의 이런 형태로 데코레이터가 사용됩니다.

>>> @decorate
... def say_bye():
...     print("Bye~")
...
...
>>> say_bye()
before
Bye~
after

데코레이터로 원래 함수의 인자 그대로 넘기기

이번에는 인자를 받는 함수에 @decorate 데코레이터를 적용해보겠습니다.

>>> @decorate
... def say(msg):
...     print(msg)
...
...
>>> say("How are you?")
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    say("How are you?")
TypeError: wrapper() takes 0 positional arguments but 1 was given

예외가 발생하는 이유는, 데코레이터 내부의 wrapper() 함수가 원래 인자를 무시해버렸기 때문입니다. 원래 함수에서 넘어온 인자를 그대로 데코레이터의 내부 함수로 넘기려면 *args**kwargs를 사용해야 합니다.

>>> def decorate(func):
...     def wrapper(*args, **kwargs):
...         print("before")
...         func(*args, **kwargs)
...         print("after")
...     return wrapper

인자를 받는 say() 함수에 수정된 데코레이터를 적용해보면 정상적으로 작동하는 것을 볼 수 있습니다.

>>> @decorate
... def say(msg):
...     print(msg)
...
...
>>> say("How are you?")
before
How are you?
after

데코레이터로 부터 원래 함수의 리턴값 그대로 받기

이번에는 어떤 값을 리턴하는 함수에 위에서 작성한 데코레이터를 선언해보겠습니다.

>>> @decorate
... def give_hi():
...     return "Hi!"
...
>>> result = give_hi()
before
after
>>> print(result)
None

원래 함수에서 리턴한 "Hi!"가 데코레이터를 적용한 이후에는 리턴되지가 않는 것을 볼 수 있습니다. 이유는, 데코레이터의 wrapper() 함수에서 원래 리턴값을 그대로 보존해주지 않았기 때문입니다.

원래 함수의 리턴값을 value 변수에 저장해두고 마지막에 리턴을 해주면 원래 함수의 리턴값을 그대로 받아올 수 있습니다.

>>> def decorate(func):
...     def wrapper(*args, **kwargs):
...         print("before")
...         value = func(*args, **kwargs)
...         print("after")
...         return value
...     return wrapper
...
>>> @decorate
... def give_hi():
...     return "Hi"
...
>>> result = give_hi()
before
after
>>> print(result)
Hi

@functools.wraps 활용

데코레이터를 사용할 때 한 가지 문제점은 원래 함수의 메타 정보가 데코레이터의 메타 정보로 대체된다는 것입니다.

>>> @decorate
... def say_hi():
...     print("Hi!")
...
...
>>> say_hi
<function decorate.<locals>.wrapper at 0x10e5690d0>
>>> say_hi.__name__
'wrapper'

이를 방지하기 위해서는 데코레이터를 작성할 때 내부 함수 위에 @functools.wraps 데코레이터를 선언해줘야 합니다.

>>> def decorate(func):
...     @wraps(func)
...     def wrapper(*args, **kwargs):
...         print("before")
...         value = func(*args, **kwargs)
...         print("after")
...         return value
...     return wrapper
...
>>> @decorate
... def say_hi():
...     print("Hi!")
...
...
>>> say_hi
<function say_hi at 0x10e569f70>
>>> say_hi.__name__
'say_hi'

@functools.wraps 데코레이터에 대한 자세한 내용은 파이썬 공식 레퍼런스를 참고 바랍니다.

마치면서

이상으로 파이썬에서 데코레이터를 작성하고 다른 함수에 적용하는 방법에 대해서 매우 간단히 살펴보았습니다.