Python 데코레이터의 함수 인자와 리턴값 처리

Ad Placeholder

앞에서 기본적인 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와는 다른 형태로, 데코레이터 자체에 원하는 값을 전달하는 방법을 알아볼 예정입니다.

jiji

AI, Python, 생활정보와 정부 정책 정보를 쉽게 설명하는 정보 블로그입니다.

Ad Placeholder
Scroll to Top