데코레이터는 파이썬에서 강력한 기능을 제공한다. 알아두면 도움이 많이 될 것이고 다른 사람 코드에도 자주 보이기 때문에 알아둬야 한다.
중요 키워드
임포트 타임, 런타임
global
자유변수
nonlocal
functools.wraps, functools.lru_cache, functools.singledispatch
덜 중요한 키워드
메타프로그래밍
__code__.co_varnames
, __code__.co_freevars
, __closure__[0].cell_contents
abc.MutableSequence, numbers.Integral
참고 자료
http://bit.ly/1DePVRi
http://bit.ly/1DePPcl
http://wrapt.readthedocs.org/en/lastest/
https://pypi.python.org/pypi/decorator
https://wiki.python.org/moin/PythonDecoratorLibrary
http://www.python.org/dev/peps/pep-0443
http://www.artima.com/weblogs/viewpost.jsp?thread=101605
http://reg.readthedocs.org/en/lastest
http://effbot.org/zone/closure.htm
http://www.python.org/dev/peps/pep-3104/
http://bit.ly/mccarthy_recursive
데코레이터는 특정 상황의 코드를 편리하게 나타내는 구문이다.
def f(func):
print('function called')
return func
def g(x, y, /):
return x + y
g = f(g)
위와 같은 코드는 아래와 같이 데코레이터를 통해 나타낼 수 있다.
def f(func):
print('function called')
return func
@f
def g(x, y, /):
return x + y
데코레이터는 런타임에 프로그램을 제어할 수 있는 메타프로그래밍을 할 때 유용하게 쓰인다. 데코레이터를 적용하면 g=f(g)
는 임포트 타임에 실행된다. 위 코드를 import하면 function called가 출력될 것이다.
데코레이터는 임포트될 때 한 번만 실행되기 때문에 임포트 타임, 데코레이트된 함수는 명시적으로 호출해야 실행되기 때문에 런타임에 실행된다고 한다.
이전 글에서 전략 패턴에서의 전략들을 리스트에 담는 방법으로 데코레이터를 이용하는 방법도 있다는 언급이 있었다. 아래와 같이 함수를 짜고 전략 함수마다 데코레이트 해주면 된다.
strategy_function_list = []
def register(func):
strategy_function_list.append(func)
return func
@register
def strategy1():
pass
@register
def strategy2():
pass
이렇게 하면 임포트할 때 register가 표시된 함수들만 strategy_function_list에 담긴다. 일반적으로는 데코레이터를 정의하는 부분과 적용하는 부분을 분리하여 다른 모듈로 만든다.
함수 안에서 어떤 변수가 대입되는 명령이 있으면 그 변수는 지역변수로 간주된다. 그렇지 않으면 그 지역에서 변수를 바인딩하지 못할 경우 전역변수를 탐색한다.
아래 코드는 오류가 없다.
a = 3
def f():
print(a)
f()
하지만 아래 코드는 오류가 발생한다.
a = 3
def f():
print(a) #a가 지역변수로 간주되기 때문에 바깥 a를 찾을 수 없음. 오류 발생
a = 0
f()
아래처럼 수정하여 해결할 수 있다.
a = 3
def f():
global a
print(a)
a = 0
f()
global a는 변수 a가 등장하기 이전에 선언해야 한다.
클로저는 함수를 선언하여 반환하는 함수이다. 여러가지 파라미터를 입력받아 새로운 함수를 만들어내는 함수로써 사용할 수 있다. 아래 코드처럼 클로저 안에 있지만 반환될 내부 함수 바깥에 있는 a는 원래라면 return inner
가 실행되면서 메모리 해제가 되어야 하지만 클로저에서는 자유변수
라는 이름으로 inner함수에 귀속된다.
def closer():
a = []
def inner(b):
a.append(b)
print(a)
return inner
c = closer()
c(3) #[3]
c(2) #[3,2]
c.__code__.co_freevars
를 통해 자유변수 목록을 확인할 수 있고 자유변수는 c.__code__.co_varnames
에 나오지 않는다. c.__closure__[0].cell_contents
를 통해 직접 a에 접근할 수 있다.
위 같은 상황에서 a에 다른 값을 대입하는 연산을 하면 a를 지역변수로 만들어버리기 때문에 오류가 난다. 대입을 못한다는 뜻은 a가 불변객체일 경우 의미가 없어진다는 뜻이다. nonlocal a를 선언하여 a에 대입하는 연산을 하더라도 자유변수로 그대로 남을 수 있도록 할 수 있다. 따라서 a가 불변 객체여도 nonlocal을 선언했다면 자유변수로 사용할 수 있다.
데코레이터를 사용하여 함수를 호출하면 실행시간까지 측정해서 출력해주는 기능을 만들수도 있다.
데코레이터를 적용하면 __name__
와 __doc__
이 데코레이터에서 반환하는 함수의 것으로 덮어씌워진다. 이때 __name__
과 __doc__
를 데코레이트 되는 함수의 것으로 유지하고 싶으면 @functools.wraps(데코레이터의 입력받은 함수)
를 데코레이터에서 반환하는 함수에 데코레이트 하여 해결할 수 있다.
functools.lru_cache()를 데코레이트 하여 해당 함수를 최적화 할 수 있다. 파라미터 입력에 따른 결과값을 메모이제이션 하고, 정해진 메모리 용량을 넘기면 가장 오랫동안 쓰이지 않은 데이터를 삭제하여 공간을 비운다. 메모이제이션은 내부적으로 dict를 통해 이뤄지므로 모든 파라미터의 인자들은 해시가능이어야 한다.
functools.singledispatch를 통해 다양한 자료형에 특화된 함수들을 하나로 묶을 수 있다. singledispatch로 데코레이트 하면 데코레이트 된 함수에 register함수가 생기는데 이를 타입을 넣어 다른 함수에 데코레이트 시키면 된다. 타입을 넣을 때 시퀀스의 경우 abc.MutableSequence, int의 경우 numbers.Integral으로 하는 것이 좋다.
데코레이터는 여러 개를 누적시켜도 된다. 차례대로 작동한다.
매개변수를 받아 유연하게 데코레이터를 생성하는 데코레이터 팩토리도 있다. 데코레이터를 적용할 때 한 번의 함수 호출을 거쳐 적용해야 한다. 이 함수 호출에서 파라미터를 넘겨줄 수 있다. functools.lru_cache가 그 예시이다.