WeniVooks

검색

견고한 파이썬

데코레이터

1. 데코레이터란?

파이썬 데코레이터는 기본적으로 함수 또는 메서드를 꾸며주는 함수입니다. 이러한 코드는 파이썬의 프레임웤인 Django에서는 아래와 같이 활용될 수 있습니다.

def login(function):
    pass
 
@login
def 게시판읽기():
    pass
def login(function):
    pass
 
@login
def 게시판읽기():
    pass

위 코드는 로그인한 사용자만 게시판을 읽을 수 있게 합니다. 이처럼 어떤 조건을 선행해야 할 때에 사용할 수 있습니다. 선행 조건에서만 사용할 수 있는 것은 아닙니다.

def 시간체크(function):
    pass
 
@시간체크
def f():
    pass
def 시간체크(function):
    pass
 
@시간체크
def f():
    pass

위 코드는 함수 f()의 실행 시간을 체크할 수 있습니다. 그렇다면 실행 전에 한 번 시간을 체크하고, 실행이 끝난 후에도 시간을 체크해야 합니다.

def log(function):
    pass
 
@log
def f():
    pass
def log(function):
    pass
 
@log
def f():
    pass

위 코드는 함수의 애러나 결과를 로그로 남길 수 있습니다. 이러한 로그는 파일로 관리하여 날짜별로도 관리할 수 있습니다.

이렇게 데코레이터는 어떤 함수가 동작하기 전과 후에서 어떤 동작을 할 수 있게 합니다.

1.1 데코레이터의 작동원리

데코레이터는 고차 함수입니다. 앞서 설명한것처럼 고차 함수는 하나 이상의 함수를 인자로 받고, 함수를 결과로 반환하는 함수입니다. 데코레이터도 마찬가지로 하나의 함수를 인자로 받아, 기능이 추가 또는 변경된 새로운 함수를 반환합니다. 기본적인 데코레이터의 작성과 사용에 대해서 단계별로 알아봅시다.

def simple_decorator(function): def wrapper(): print("전") function() print("후") return wrapper @simple_decorator def hello(): print("Hello, World!") hello() # 데코레이터가 없는 상태에서는 simple_decorator(hello)() 와 같습니다.
1.1.1 데코레이터 함수 정의

먼저, simple_decorator라는 데코레이터 함수를 정의합니다. 이 함수는 함수를 인자로 받아 인자로 반환합니다. 여기서 중요한 점은, 인자로 받는 function파라미터는 실제 실행되는 함수(위 예제에셔는 hello)이며 반환되는 함수(wrapper) 내에서 인자로 받은 함수(function)가 호출된다는 것입니다. 이를 통해 function의 기능을 확장하게 됩니다.

def simple_decorator(function):
    def wrapper():
        print('전')
        function()
        print('후')
    return wrappe
def simple_decorator(function):
    def wrapper():
        print('전')
        function()
        print('후')
    return wrappe
1.1.2 데코레이터 적용

데코레이터를 적용하는 방법은 두 가지입니다. 첫 번째 방법은 @simple_decorator 문법을 사용하여 직접 함수 정의 위에 데코레이터를 선언하는 것이고, 두 번째 방법은 함수 객체를 데코레이터 함수의 인자로 전달하는 것입니다. 두 방법은 동일한 결과를 가져옵니다.

# 방법1: 데코레이터 사용
@simple_decorator
def hello():
    print("Hello, World!")
hello()
# 방법1: 데코레이터 사용
@simple_decorator
def hello():
    print("Hello, World!")
hello()
# 방법2 데코레이터 미사용
def hello():
    print("Hello, World!")
simple_decorator(hello)()
# 방법2 데코레이터 미사용
def hello():
    print("Hello, World!")
simple_decorator(hello)()
1.1.3 확장된 함수 호출

데코레이터를 적용한 후 hello() 함수를 호출하면, 사실은 wrapper 함수가 호출됩니다. 이 wrapper 함수 내에서 원래의 hello 함수가 function이라는 이름으로 호출되며, 추가로 정의된 문자열 출력 동작이 수행됩니다.

hello() # 데코레이터가 없는 상태에서는 simple_decorator(hello)() 와 같습니다.
hello() # 데코레이터가 없는 상태에서는 simple_decorator(hello)() 와 같습니다.
1.1.4 결과

코드를 실행하면 다음과 같이 출력됩니다. 이는 데코레이터를 사용하여 hello 함수의 동작을 확장한 결과입니다. 데코레이터를 사용하면 코드의 재사용성을 높이고, 코드를 더 깔끔하고 모듈화된 형태로 유지할 수 있습니다.

데코레이터를 직접 @ 로 사용하지 않고 함수로 정리를 하자면 아래와 같습니다. step1까지는 wrapper를 호출한 것이고, step2에 마지막 뒤에 있는 괄호는 step1의 wrapper를 wrapper()의 형태로 호출한 것입니다. 이는 @로 호출한 것과 동일한 결과입니다.

def simple_decorator(function): def wrapper(): print('전') function() print('후') return wrapper def hello(): print("Hello, World!") simple_decorator(hello)() # step1: simple_decorator(hello) => wrapper # step2: simple_decorator(hello)() => wrapper()
1.2 매개변수가 있는 함수의 데코레이터
def simple_decorator(function):
    def wrapper():
        print('전')
        function()
        print('후')
    return wrapper
 
@simple_decorator
def hello(name):
    print(f'Hello, {name}!')
 
hello(name) # 여기에 name을
def simple_decorator(function):
    def wrapper():
        print('전')
        function()
        print('후')
    return wrapper
 
@simple_decorator
def hello(name):
    print(f'Hello, {name}!')
 
hello(name) # 여기에 name을

만약 매개변수가 포함된 함수에 데코레이터를 적용시키려면 어떻게 해야 할까요? 데코레이터를 사용하여 매개변수가 있는 함수를 수정하려면, 데코레이터 내의 wrapper 함수도 동일한 매개변수를 받을 수 있어야 합니다.

1.2.1 매개변수가 있는 데코레이터 함수 정의

데코레이터 함수를 정의할 때, 내부의 wrapper 함수에 매개변수를 정의해줍니다. 이렇게 해야 데코레이터가 원래 함수의 매개변수를 받을 수 있습니다.

def simple_decorator(function): def wrapper(a, b): # point print('전') result = function(a, b) print('후') return result return wrapper @simple_decorator def hello(a, b): return a + b hello(10, 20)

위 코드에서 hello(10, 20)에 넣은 10과 20의 아규먼트는 실제로 point라고 주석을 달아놓은 wrapper의 a와 b에 할당됩니다. 여기서 주의해야 할 점은 function의 result를 저장하지 않으면 return값으로 30을 받을 수 없다는 것입니다. 데코레이터가 있는 함수의 return 값은 실제로는 wapper의 return값입니다.

이는 아래와 같이 표현될 수 있습니다.

def simple_decorator(function): def wrapper(a, b): print('전') result = function(a, b) print('후') return result return wrapper def hello(a, b): return a + b simple_decorator(hello)(10, 20) # step1: simple_decorator(hello) => wrapper # step2: simple_decorator(hello)(a, b) => wrapper(a, b)
1.3 데코레이터 응용

다음은 데이터 전처리를 수행하는 data_Preprocessing 데코레이터와 그 데코레이터가 적용된 mean 함수를 정의하고 있습니다. 먼저 각 스텝별로 코드가 어떻게 오류를 수정해 나가는지 살펴보고 향후에 오류 발생시 적용해봅시다.

# step 1
def data_Preprocessing(function):
    def wrapper():
        pass
    return wrapper
 
@data_Preprocessing
def mean(data):
    return sum(data)/len(data)
 
mean([1, 2, '3', 4, '5']) # TypeError 발생
 
# step 2
def data_Preprocessing(function):
    def wrapper(data):
        print(data)
    return wrapper
 
@data_Preprocessing
def mean(data):
    return sum(data)/len(data)
 
mean([1, 2, '3', 4, '5']) # 데이터만 출력하고 함수는 None을 반환
 
# step 3
def data_Preprocessing(function):
    def wrapper(data):
        return function(list(map(int, data)))
    return wrapper
 
@data_Preprocessing
def mean(data):
    return sum(data)/len(data)
 
mean([1, 2, '3', 4, '5']) # 출력: 3.0 (정상 작동)
# step 1
def data_Preprocessing(function):
    def wrapper():
        pass
    return wrapper
 
@data_Preprocessing
def mean(data):
    return sum(data)/len(data)
 
mean([1, 2, '3', 4, '5']) # TypeError 발생
 
# step 2
def data_Preprocessing(function):
    def wrapper(data):
        print(data)
    return wrapper
 
@data_Preprocessing
def mean(data):
    return sum(data)/len(data)
 
mean([1, 2, '3', 4, '5']) # 데이터만 출력하고 함수는 None을 반환
 
# step 3
def data_Preprocessing(function):
    def wrapper(data):
        return function(list(map(int, data)))
    return wrapper
 
@data_Preprocessing
def mean(data):
    return sum(data)/len(data)
 
mean([1, 2, '3', 4, '5']) # 출력: 3.0 (정상 작동)
1.4 중첩 데코레이터

데코레이터를 하나만 쓸때는 큰 문제가 없지만 데코레이터를 여러 개 사용할 때 그 작동 원리를 이해하는 것은 중요합니다. 먼저 데코레이터가 적용되는 순서에 대해 좀 더 상세히 알아보겠습니다. 하나의 함수에는 아래와 같은 방식으로 여러 개의 데코레이터를 붙일 수 있습니다.

여러 개의 데코레이터를 사용할 때, 이들은 함수에 decorator3, decorator2, decorator1 차례로 적용되며, 각각의 데코레이터는 이전 데코레이터에 의해 반환된 함수를 새로 감싸게 됩니다.

@decorator1
@decorator2
@decorator3
def my_func():
    pass
@decorator1
@decorator2
@decorator3
def my_func():
    pass

데코레이터를 사용하지 않으면 다음과 같이 나타내야합니다.

my_func = decorator1(decorator2(decorator3(my_func)))
my_func = decorator1(decorator2(decorator3(my_func)))

각 데코레이터가 문자열의 끝에 특정 문자를 추가하는 기능을 갖도록 설정합니다.

def add_exclamation(function):
    def wrapper(text):
        print(f'add_exclamation 데코레이터 시작')
        result = function(text) + "!"
        print(f'add_exclamation 데코레이터 종료')
        return result
    return wrapper
 
def add_question_mark(function):
    def wrapper(text):
        print(f'add_question_mark 데코레이터 시작')
        result = function(text) + "?"
        print(f'add_question_mark 데코레이터 종료')
        return result
    return wrapper
 
def add_dot(function):
    def wrapper(text):
        print(f'add_dot 데코레이터 시작')
        result = function(text) + "."
        print(f'add_dot 데코레이터 종료')
        return result
    return wrapper
def add_exclamation(function):
    def wrapper(text):
        print(f'add_exclamation 데코레이터 시작')
        result = function(text) + "!"
        print(f'add_exclamation 데코레이터 종료')
        return result
    return wrapper
 
def add_question_mark(function):
    def wrapper(text):
        print(f'add_question_mark 데코레이터 시작')
        result = function(text) + "?"
        print(f'add_question_mark 데코레이터 종료')
        return result
    return wrapper
 
def add_dot(function):
    def wrapper(text):
        print(f'add_dot 데코레이터 시작')
        result = function(text) + "."
        print(f'add_dot 데코레이터 종료')
        return result
    return wrapper

다음으로, 위에서 정의한 데코레이터들을 사용하여 한 함수에 중첩적용해보겠습니다.

@add_exclamation
@add_question_mark
@add_dot
def greet(message):
    return message
@add_exclamation
@add_question_mark
@add_dot
def greet(message):
    return message

이제 위의 코드를 실행한 후, greet 함수에 문자열 인자를 전달하여 호출해보겠습니다.

result = greet("Hello")
print(result)  # 출력: "Hello.?!"
result = greet("Hello")
print(result)  # 출력: "Hello.?!"

출력 결과를 통해 데코레이터가 어떻게 중첩적으로 적용되는지 확인할 수 있습니다. 데코레이터는 아래에서 위로, 즉 add_dot부터 시작하여 add_exclamation까지 차례대로 적용되며, 각 데코레이터는 이전 데코레이터의 결과를 기반으로 작동합니다.

1.5 동적 데코레이터

동적 데코레이터는 파이썬에서 데코레이터를 생성할 때 추가적인 인자를 받을 수 있게 하는 기능을 가리킵니다. 동적 데코레이터를 작성하는 방법은 데코레이터를 반환하는 함수를 만드는 것입니다.

def add(n): def decorator(function): def new_function(a, b): print(f'plus 함수가 {n}만큼 증가시키는 데코레이터가 시작됩니다.') result = function(a, b) print(f'plus 함수가 {n}만큼 증가시키는 데코레이터가 종료됩니다.') return result + n return new_function return decorator @add(1000) def plus(a, b): print('plus 함수가 호출되었습니다.') return a + b result = plus(10, 20) print(f'result : {result}')

동적 데코레이터는 다양한 환경에서 유용하게 사용됩니다. 예를 들어, 로깅 레벨을 조절하는 데코레이터, 결과 캐시 사이즈를 조절하는 데코레이터 등 다양한 환경에서 동적 데코레이터를 활용하여 코드의 재사용성을 높일 수 있습니다.

1.6 클래스형 데코레이터

데코레이터는 함수형 데코레이터뿐만 아니라 클래스형 데코레이터로도 작성할 수 있습니다. 이번에는 클래스형 데코레이터에 대해 알아봅시다. 클래스를 이용하여 데코레이터를 작성하려면 __call__ 매직 메소드를 사용해야 합니다. 이 메소드는 클래스의 인스턴스를 함수처럼 호출할 수 있게 해줍니다.

class Debug: def __init__(self, function): self.function = function def __call__(self): print(f'{self.function.__name__} 함수 시작') result = self.function() # 인자 없이 원래 함수 호출 print(f'{self.function.__name__} 함수 끝') return result # 함수의 결과를 반환 @Debug def f1(): print('안녕하세요') @Debug def f2(): print('hello') f1() f2()
# 출력
f1 함수 시작
안녕하세요
f1 함수 끝
f2 함수 시작
hello
f2 함수 끝
# 출력
f1 함수 시작
안녕하세요
f1 함수 끝
f2 함수 시작
hello
f2 함수 끝

클래스형 데코레이터의 원리는 __call__이라는 매직 메소드에 있습니다. __call__ 메소드를 정의하면 객체명() 형태로 함수처럼 호출이 가능하도록 합니다. 실제로 함수도 function이라는 클래스의 객체이고 __call__ 메소드를 가지고 있기 때문에 함수명() 형태로 호출이 가능한 것입니다.

def f(): pass type(f) # 출력 ㅣ <class 'function'> dir(f) # 출력 ['__annotations__', '__call__', '__class__', ... ]

아래 코드를 살펴보도록 하겠습니다.

@Debug
def f1():
    print('안녕하세요')
@Debug
def f1():
    print('안녕하세요')

위 코드는 아래와 같이 Debug 클래스로 생성한 인스턴스를 call한 것입니다.

Debug(f1)()
Debug(f1)()

전체 코드는 아래와 같습니다.

class Debug:
    def __init__(self, function):
        self.function = function

    def __call__(self):
        print(f'{self.function.__name__} 함수 시작')
        result = self.function()  # 인자 없이 원래 함수 호출
        print(f'{self.function.__name__} 함수 끝')
        return result  # 함수의 결과를 반환

def f1():
    print('안녕하세요')

Debug(f1)()
class Debug:
    def __init__(self, function):
        self.function = function

    def __call__(self):
        print(f'{self.function.__name__} 함수 시작')
        result = self.function()  # 인자 없이 원래 함수 호출
        print(f'{self.function.__name__} 함수 끝')
        return result  # 함수의 결과를 반환

def f1():
    print('안녕하세요')

Debug(f1)()

2. 나아가기

2.1 데코레이터의 발전배경

데코레이터는 Python 2.4 버전에서 도입되어 현재까지 여러 파이썬 버전에서 활용되고 있습니다. 이 기능의 도입 배경과 초기 설계 의도는 PEP 318에서 확인할 수 있습니다. 이 문서는 데코레이터가 어떤 문제를 해결하려고 도입되었는지, 그리고 데코레이터가 어떻게 파이썬 언어에 통합되었는지에 대한 히스토리를 제공합니다. 그러나 모든 기능에 대하여 항상 이런 공식 문서를 살펴보는 것을 권하지는 않습니다.

PEP 318 – Decorators for Functions and Methods | peps.python.org

💡 PEP문서는 https://www.python.org/dev/peps/pep-xxxx와 같은 형태로 배포가 됩니다. 4자리 번호가 아닐경우 앞을 0으로 채웁니다. 제안된 모든 PEP문서는 0번째 문서에서 확인할 수 있는데 이 문서는 https://www.python.org/dev/peps 에서 확인할 수 있습니다.

2.2 데코레이터의 활용

데코레이터는 캐싱, 인증, 전처리, 로깅, 모니터링 등 다양한 상황에서 활용될 수 있습니다. 여러분이 실무에서 다양한 프레임웤이나 라이브러리를 사용하실 때 그곳에서 지원하는 다양한 데코레이터를 사용할 수도 있습니다.

데코레이터는 파이썬 프로그래밍에서 코드를 보다 효율적으로 관리하고, 함수와 메소드에 추가 기능을 부여하는 데 유용한 도구입니다. 그러므로 파이썬 개발자라면 데코레이터에 대한 깊은 이해가 필요합니다.

{"packages":["numpy","pandas","matplotlib","lxml"]}
13.5 클로저13.7 함수의 다른 형태 lambda