파이썬 데코레이터는 함수나 메서드를 장식하거나 수정하는 데 사용되는 강력한 기능이다. 데코레이터는 기존 함수의 동작을 변경하거나 보완하기 위해 함수를 래핑 하고, 또는 새로운 동작을 추가할 수 있다. 이것은 함수형 프로그래밍의 개념 중 하나인 고차 함수(Higher-order functions)를 활용한 것이다. 데코레이터는 일반적으로 함수나 메서드 위에 @decorator 문법을 사용하여 적용된다.
따라서, 데코레이터는 코드 재사용성을 높이고, 가독성을 향상하며, 관심사를 분리하는 데 도움이 된다. 또한, 함수나 메서드를 수정하지 않고도 쉽게 새로운 동작을 추가할 수 있는 장점이 있다.
위의 예제 코드에서 @decorator 구문은 decorator(hello)로 해석이 된다, 즉 decorator 함수의 파라미터로 hello 함수가 전달되는 것이다. 그러나 decorator 함수 내부에서는 받은 함수(hello 함수)를 그대로 반환하고 있다. 결과적으로 hello 함수는 그대로 유지되고, 데코레이션이 변경이 없는 채로 호출된다. 따라서 실행 결과는 아래와 같다.
Initiate Decorator
hello world
실행 결과
📢 그럼 decorator 함수 내부에서 받은 함수를 그대로 반환하지 않고 커스터마이즈 할 수 있을까?
그렇다, 데코레이터 함수 내부에서 받은 함수를 커스터마이징하여 변경한 후 반환할 수 있다. 데코레이터는 함수를 수정하거나 감싸는 역할을 하는데, 이때 받은 함수를 변경하여 새로운 동작을 추가하거나 수정할 수 있다. 예를 들어, 데코레이터 함수 내부에서 받은 함수에 어떤 동작을 추가하고 그 결과를 반환하는 식으로 변경할 수 있는 아래의 예제를 한 번 살펴보자.
def decorator(func):
def wrapper():
print('Initiate Decorator')
result = func() # 기존 함수 실행
print('Decorator completed')
return result
return wrapper
@decorator
def hello():
print('hello world')
hello()
이제 wrapper 함수가 받은 함수(hello 함수)를 호출하면서 앞뒤로 추가적인 동작을 수행한다.
Initiate Decorator
hello world
Decorator completed
실행 결과
앞에서 언급했듯이 데코레이터 내부에서 받은 함수를 직접 변경 또한 가능하다. 이를 위해서는 내부 함수인 wrapper에서 받은 함수를 직접 수정하고자 하는 내용을 적요하면 된다.
def decorator(func):
def wrapper():
print('Initiate Decorator')
# 받은 함수를 직접 수정
func.__doc__ = "Modified docstring"
print(func.__doc__) # 수정된 docstring 출력
# 받은 함수 실행
result = func()
print('Decorator completed')
return result
return wrapper
@decorator
def hello():
"""
Original docstring
"""
return 'hello world'
print(hello())
Initiate Decorator
Modified docstring
Decorator completed
hello world
실행 결과
위의 예제에서 wrapper 함수 내에서 func.__doc__을 통해 받은 함수의 docstring을 변경하고 있다. 이런 식으로 함수의 속성을 변경하거나 추가할 수 있지만, 이렇게 직접 함수를 수정하는 것은 신중히 다뤄야 한다. 함수를 수정함으로써 의도치 않은 부작용이 발생할 수 있으며, 코드를 이해하기 어려워질 수 있기 때문이다. 따라서, 함수를 수정하는 대신, 추가적인 동작을 데코레이터에 넣는 것이 일반 적으러 더 권장되는 방식이다.
📢 지금까지는 파라미터를 받지 않는 hello 함수를 이용해서 decorator를 사용해 봤다, 그러면 파라미터를 받은 함수에 decorator를 사용하고 싶으면 어떻게 해야 할까?
def decorator(func):
print('Initiate Decorator')
def wrapper(x, y):
print('-- wrapper function in decorator initiated --')
print("Adding numbers :", x, " and ", y)
result = func(x, y)
print("Result : ", result)
return result
return wrapper
@decorator
def add(x,y):
return x + y
print(add(1,2))
Initiate Decorator
-- wrapper function in decorator initiated --
Adding numbers : 1 and 2
Result : 3
3
실행 결과
wrapper 함수에서 x와 y 파라미터는 데코레이터가 적용된 함수인 add 함수로부터 전달된다. 다시 언급하자면, @decorator 문법은 위의 예제 코드에서 add 함수를 감싸는 역할을 한다. 즉, add 함수가 호출될 때, 실제로는 wrapper 함수가 호출되어 파라미터를 처리하고 그 결과를 반환한다.
즉, wrapper 함수의 파라미터 x와 y는 데코레이터가 적용된 함수의 매개변수와 동일하게 전달된다.
📢 그럼 decorator 함수 안에 있는 wrapper라는 이름은 파이썬에서 미리 약속된 키워드라 꼭 wrapper로 사용해야 하나?
아니다, wrapper라는 이름은 강제사항이 아니고, 데코레이터 함수 내부에서 사용되는 래핑 함수의 이름은 개발자가 선택할 수 있다. wrapper는 그저 일반적으로 사용되는 이름 중 하나일 뿐이다. 일반적으로 데코레이터 함수 내부에서 사용되는 래핑 함수의 이름은 wrapper나 wrapped와 같은 일반적인 관례에 따르기도 한다. 그러나 실제로는 어떤 이름을 사용해도 무방하다.
📢 하나의 함수에 두 개 이상의 데코레이터도 적용시킬 수 있는가?
그렇다, 함수에 여러 개의 데코레이터를 적용할 수 있다. 파이썬에서는 함수 정의 위에 여러 데코레이터를 중첩해서 사용할 수 있다. 이때, 가장 아래쪽의 데코레이터가 가장 먼저 적용되며, 그다음으로 위로 순서대로 적용된다.
def decorator1(func):
def wrapper(x):
result = func(x)
print(f'Decorator 1 result : {result}')
return result
return wrapper
def decorator2(func):
def wrapper(x):
result = func(x)
print(f'Decorator 2 result : {result}')
return result
return wrapper
@decorator1
@decorator2
def add(x):
x += 1
return x
print(add(1))
Decorator 2 result : 2
Decorator 1 result : 2
2
실행 결과
데코레이터는 아래에서 위로 순서대로 적용된다. 따라서, 위의 코드에서는 @decorator2가 먼저 적용되고, 그다음에 @decorator1이 적용된다.
📢 그럼 위의 2개의 decorator를 적용시킨다고 했을 때 이 둘은 독립적으로 적용되는 건가? 아니면 decorator1의 반환값이 decorator2에 영향을 미칠까?
데코레이터가 여러 개인 경우, 각 데코레이터는 독립적으로 적용되며, 반환값이 다음 데코레이터에 전달되지 않는다. 즉, 데코레이터 간에 반환값을 전달하거나 영향을 미치지 않는다.