앞에서 기본적인 Python 데코레이터의 구조를 살펴봤습니다.
데코레이터는 기존 함수에 새로운 기능을 추가할 수 있다는 장점이 있지만, 한 가지 문제가 있었습니다.
바로 함수의 인자와 반환값을 제대로 처리하지 못한다는 것입니다.
예를 들어 다음과 같은 함수가 있다고 해보겠습니다.
def greet(name):
print(f"{name}님 안녕하세요")
이 함수에 앞에서 만든 데코레이터를 그대로 적용하면 wrapper()가 name을 전달받지 못합니다.
또한 원래 함수가 값을 반환하는 경우에는 그 반환값까지 그대로 전달해줘야 합니다.
이번 글에서는 데코레이터가 함수의 인자와 반환값을 어떻게 처리하는지 알아보겠습니다.
인자가 없는 데코레이터의 기본 구조
먼저 앞에서 사용했던 가장 단순한 데코레이터를 다시 살펴보겠습니다.
def decorator(func):
def wrapper():
print("함수 실행 전")
func()
print("함수 실행 후")
return wrapper
다음과 같이 사용할 수 있습니다.
@decorator
def greet():
print("안녕하세요")
이 구조에서는 문제가 없습니다.
greet()가 아무런 인자를 받지 않기 때문입니다.
greet()
하지만 인자를 받는 함수를 데코레이터로 감싸면 문제가 생깁니다.
@decorator
def greet(name):
print(f"{name}님 안녕하세요")
다음과 같이 호출하면,
greet("Python")
wrapper()가 "Python"이라는 인자를 받을 수 없기 때문에 오류가 발생합니다.
현재 wrapper()가 다음처럼 정의되어 있기 때문입니다.
def wrapper():
...
따라서 데코레이터가 다양한 형태의 함수를 처리하려면 wrapper()가 함수의 인자를 받을 수 있도록 만들어야 합니다.
*args로 위치 인자 전달하기
이때 앞에서 배웠던 *args를 사용할 수 있습니다.
def decorator(func):
def wrapper(*args):
print("함수 실행 전")
func(*args)
print("함수 실행 후")
return wrapper
이제 인자를 받는 함수에도 사용할 수 있습니다.
@decorator
def greet(name):
print(f"{name}님 안녕하세요")
호출해보겠습니다.
greet("Python")
결과:
함수 실행 전
Python님 안녕하세요
함수 실행 후
여기서 중요한 부분은 두 곳입니다.
def wrapper(*args):
그리고
func(*args)
첫 번째는 wrapper()가 전달받은 여러 위치 인자를 args에 패킹하는 것입니다.
두 번째는 패킹된 args를 다시 원래 함수에 언패킹해서 전달하는 것입니다.
즉,
함수 호출
↓
wrapper(*args)
↓
args에 인자 패킹
↓
func(*args)
↓
원래 함수에 인자 언패킹
이 구조가 됩니다.
여러 개의 인자도 처리할 수 있다
*args를 사용하면 인자가 몇 개인지 미리 알 필요가 없습니다.
def decorator(func):
def wrapper(*args):
print("실행 전")
result = func(*args)
print("실행 후")
return result
return wrapper
예를 들어 다음 함수에 적용할 수 있습니다.
@decorator
def add(a, b):
return a + b
호출하면,
result = add(10, 20)
print(result)
결과:
실행 전
실행 후
30
add()에 전달된 10, 20이 args에 들어갔다가 다시 func(*args)를 통해 원래 함수로 전달됩니다.
**kwargs로 키워드 인자 처리하기
이번에는 키워드 인자를 생각해보겠습니다.
다음 함수는 키워드 인자를 사용할 수 있습니다.
def greet(name, age):
print(f"{name}님은 {age}살입니다.")
다음과 같이 호출할 수도 있습니다.
greet(name="Python", age=10)
이런 키워드 인자를 데코레이터에서 처리하려면 **kwargs가 필요합니다.
def decorator(func):
def wrapper(**kwargs):
print("실행 전")
func(**kwargs)
print("실행 후")
return wrapper
이제 적용해보겠습니다.
@decorator
def greet(name, age):
print(f"{name}님은 {age}살입니다.")
greet(name="Python", age=10)
결과:
실행 전
Python님은 10살입니다.
실행 후
**kwargs는 키워드 인자를 딕셔너리 형태로 받아서 처리합니다.
*args와 **kwargs를 함께 사용하기
실제 데코레이터에서는 위치 인자와 키워드 인자를 모두 처리해야 하는 경우가 많습니다.
이때는 *args와 **kwargs를 함께 사용합니다.
def decorator(func):
def wrapper(*args, **kwargs):
print("함수 실행 전")
result = func(*args, **kwargs)
print("함수 실행 후")
return result
return wrapper
이 구조는 데코레이터에서 매우 자주 사용됩니다.
왜냐하면 원래 함수가 어떤 인자를 받을지 데코레이터를 만들 때 미리 알 수 없는 경우가 많기 때문입니다.
위치 인자와 키워드 인자를 모두 사용하기
다음과 같은 함수가 있다고 해보겠습니다.
@decorator
def introduce(name, age, job):
return f"{name}은 {age}살이고 {job}입니다."
다음처럼 호출할 수 있습니다.
result = introduce("Python", age=10, job="개발자")
print(result)
결과:
함수 실행 전
함수 실행 후
Python은 10살이고 개발자입니다.
여기서
"Python"
은 위치 인자로 args에 들어가고,
age=10
job="개발자"
는 키워드 인자로 kwargs에 들어갑니다.
그리고
func(*args, **kwargs)
를 통해 원래 함수에 그대로 전달됩니다.
왜 *args와 **kwargs를 함께 사용하는가?
데코레이터는 특정 함수 하나에만 사용할 목적으로 만드는 경우도 있지만, 여러 함수에 공통적으로 적용하기 위해 만드는 경우가 많습니다.
예를 들어 다음과 같은 함수들이 있다고 해보겠습니다.
def add(a, b):
return a + b
def greet(name):
return f"{name}님 안녕하세요"
def introduce(name, age):
return f"{name}은 {age}살입니다."
각 함수가 받는 인자의 개수와 형태가 서로 다릅니다.
이때 데코레이터를 특정 인자에 맞춰 만들면 재사용하기 어렵습니다.
def decorator(func):
def wrapper(a, b):
...
이렇게 만들면 a, b 두 개를 받는 함수에만 적합합니다.
반면 다음과 같이 만들면 다양한 함수에 적용할 수 있습니다.
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
*args와 **kwargs를 이용해 원래 함수의 인자를 그대로 전달하기 때문입니다.
함수의 반환값 처리하기
인자만큼 중요한 것이 반환값입니다.
다음과 같은 함수가 있다고 해보겠습니다.
def add(a, b):
return a + b
이 함수는 30을 반환합니다.
result = add(10, 20)
print(result)
결과:
30
그런데 데코레이터에서 원래 함수의 반환값을 제대로 처리하지 않으면 문제가 생길 수 있습니다.
예를 들어 다음과 같이 작성하면,
def decorator(func):
def wrapper(*args, **kwargs):
print("실행 전")
func(*args, **kwargs)
print("실행 후")
return wrapper
func()가 반환한 값을 저장하거나 반환하지 않습니다.
따라서 다음 코드에서,
@decorator
def add(a, b):
return a + b
result = add(10, 20)
print(result)
result는 None이 됩니다.
원래 add()는 30을 반환했지만 데코레이터의 wrapper()가 그 값을 다시 반환하지 않았기 때문입니다.
return으로 원래 함수의 반환값 전달하기
이 문제는 간단하게 해결할 수 있습니다.
원래 함수의 결과를 변수에 저장한 뒤 다시 반환하면 됩니다.
def decorator(func):
def wrapper(*args, **kwargs):
print("실행 전")
result = func(*args, **kwargs)
print("실행 후")
return result
return wrapper
이제 다시 사용해보겠습니다.
@decorator
def add(a, b):
return a + b
result = add(10, 20)
print(result)
결과:
실행 전
실행 후
30
원래 함수가 반환한 30이 데코레이터를 거쳐서 그대로 전달되었습니다.
반환값을 바로 return할 수도 있다
중간에 결과를 저장할 필요가 없다면 다음과 같이 작성할 수도 있습니다.
def decorator(func):
def wrapper(*args, **kwargs):
print("실행 전")
result = func(*args, **kwargs)
print("실행 후")
return result
return wrapper
또는 다음과 같이 작성할 수도 있습니다.
def decorator(func):
def wrapper(*args, **kwargs):
print("실행 전")
result = func(*args, **kwargs)
print("실행 후")
return result
return wrapper
데코레이터에서 실행 전후에 추가적인 처리가 필요하다면 result를 변수에 저장하는 방식이 더 편리합니다.
예를 들어 반환값을 로그로 출력하고 싶다면 다음과 같이 작성할 수 있습니다.
def decorator(func):
def wrapper(*args, **kwargs):
print("실행 전")
result = func(*args, **kwargs)
print("반환값:", result)
print("실행 후")
return result
return wrapper
가장 기본적인 데코레이터 형태
지금까지의 내용을 합치면 실무에서도 자주 볼 수 있는 기본적인 형태가 나옵니다.
def decorator(func):
def wrapper(*args, **kwargs):
print("실행 전")
result = func(*args, **kwargs)
print("실행 후")
return result
return wrapper
사용 방법은 다음과 같습니다.
@decorator
def add(a, b):
return a + b
result = add(10, 20)
print(result)
결과:
실행 전
실행 후
30
이 구조를 기억해두면 이후에 다양한 데코레이터를 이해하기 쉬워집니다.
*args와 **kwargs의 역할 다시 정리하기
앞에서 파라미터 패킹을 공부했다면 여기서 연결해서 이해할 수 있습니다.
*args는 위치 인자를 하나로 묶습니다.
def wrapper(*args):
...
그리고 *args를 다시 사용하면 묶여 있던 값을 풀어서 전달합니다.
func(*args)
**kwargs는 키워드 인자를 딕셔너리 형태로 묶습니다.
def wrapper(**kwargs):
...
그리고 **kwargs를 사용하면 다시 키워드 인자로 전달합니다.
func(**kwargs)
따라서 데코레이터에서는 다음과 같은 구조가 만들어집니다.
호출할 때
↓
args / kwargs로 패킹
↓
wrapper
↓
*args / **kwargs로 언패킹
↓
원래 함수
↓
반환값
↓
wrapper가 return
앞에서 배운 파라미터 패킹과 언패킹이 데코레이터에서 실제로 활용되는 대표적인 사례라고 볼 수 있습니다.
데코레이터를 여러 함수에 적용하기
이제 하나의 데코레이터를 여러 함수에 적용할 수 있습니다.
def log(func):
def wrapper(*args, **kwargs):
print("함수 실행:", func.__name__)
result = func(*args, **kwargs)
print("실행 완료")
return result
return wrapper
여러 함수에 적용합니다.
@log
def add(a, b):
return a + b
@log
def multiply(a, b):
return a * b
실행하면,
print(add(10, 20))
print(multiply(10, 20))
결과:
함수 실행: add
실행 완료
30
함수 실행: multiply
실행 완료
200
하나의 데코레이터로 서로 다른 인자를 받는 여러 함수를 처리할 수 있습니다.
마무리
데코레이터에서 함수의 인자와 반환값을 제대로 처리하려면 다음 구조를 기억하면 됩니다.
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
*args는 위치 인자를 처리하고, **kwargs는 키워드 인자를 처리합니다.
그리고 원래 함수가 반환한 값을 return을 통해 다시 반환해야 기존 함수의 동작을 유지할 수 있습니다.
정리하면 다음과 같습니다.
*args→ 위치 인자 처리**kwargs→ 키워드 인자 처리func(*args, **kwargs)→ 원래 함수에 인자 전달result = func(...)→ 원래 함수의 반환값 저장return result→ 반환값을 호출한 곳으로 다시 전달
이제 데코레이터가 단순히 함수를 감싸는 것에서 끝나는 것이 아니라, 원래 함수의 인자와 반환값을 유지하면서 기능을 추가할 수 있는 구조를 이해할 수 있습니다.
다음 글에서는 여기서 한 단계 더 나아가 인자를 가지는 데코레이터를 만들어보겠습니다. 지금까지의 *args, **kwargs와는 다른 형태로, 데코레이터 자체에 원하는 값을 전달하는 방법을 알아볼 예정입니다.