레이블이 decorators인 게시물을 표시합니다. 모든 게시물 표시
레이블이 decorators인 게시물을 표시합니다. 모든 게시물 표시

20130818

Python Decorators (장식자)

장식자라는 언급만 해도 공포에 질린다. 심지어 제법 훈련이 된 파이썬 프로그래머조차도 말이다.
좋다. 그렇지 않을 수도 있다. 그러나 장식자는 구문이 기이하고 어쩔 수 없이 복잡하게 구현되었다고 본인은 생각한다. 그래서 특히 파이썬의 단순함에 익숙해진 사람들에게 낯설어 보인다.

1   기본 지식

장식자는 함수를 변경한다. 더 구체적으로 장식자는 함수를 변형하는 함수이다.
장식자를 사용할 때, 파이썬은 장식될 함수를 -- 이를 목표 함수라고 부르겠다 -- 장식할 함수에 건넨다. 그리고 그 결과로 교체한다. 장식자가 없다면, 다음과 같이 보일 것이다:
# 's
 1def decorator_function(target):
 2    # 목표 함수로 일을 한다.
 3    target.attribute = 1
 4    return target
 5
 6def target(a,b):
 7    return a + b
 8
 9# 이것이 장식자가 실제로 하는 일이다.
10target = decorator_function(target)
다음 코드는 정확하게 똑같이 작동하지만, 장식자를 사용한다. 마음대로 장식자 함수의 이름을 지을 수 있다는 것을 주목하자. 여기에서는 'decorator_function'이라고 이름지었다:
# 's
1def decorator_function(target):
2    # 목표 함수로 일을 한다.
3    target.attribute = 1
4    return target
5
6# 다음은 장식자이다. 구문이 '@function_name'이다.
7@decorator_function
8def target(a,b):
9    return a + b
보시다시피, 장식자 함수의 이름 앞에 @를 둘 필요가 있다. 목표 함수 정의가 있는 줄 바로 앞에 말이다. 파이썬은 내부적으로 목표 함수를 변형한다. 장식자를 목표 함수에 적용하여 반환된 값으로 교체한다.
위의 예제 모두 결과가 같다:
# 's
1>>> target(1,2)
23
3>>> target.attribute
41

1.1   장식자 함수는 함수를 돌려주는가?

아니다. 장식자 함수는 절대로 무엇이든 돌려줄 수 있으며, 파이썬은 목표 함수를 그 반환 값으로 교체한다. 예를 들어, 다음과 같이 할 수도 있다:
# 's
 1def decorator_evil(target):
 2    return False
 3
 4@decorator_evil
 5def target(a,b):
 6    return a + b
 7
 8>>> target
 9False
10
11>>> target(1,2)
12TypeError: 'bool' object is not callable
그렇지만 일상적으로 이렇게 하고 싶지는 않을 것이다 -- 기본 디자인 원리는 함수들을 무작위로 일종의 다른 어떤 것으로 변환시키는데 있지 않다고 확신한다. 적어도 일종의 호출가능 객체를 돌려준다는 것은 말이 된다.

2   실행-시간 변형

여러분이 이렇게 외치는 소리가 들리는 듯하다. "그러나 장식자는 그 보다 더 많은 일을 한다고 생각해요. 실행-시간에 일을 하고 싶거든요. 예를 들어 조건적으로 함수를 호출하고 그 인자와 반환 값을 변형하는 일들을 말이지요."
장식자가 이런 일을 할 수 있는가? 그렇다. 정말로 위에 언급한 것보다 '더 많은 일을 하는' 것인가? 실제로는 그렇지 않다. 여기에서 너무 깊이 빠지지 않는 것이 중요하다 -- 장식자에 관하여 알아야 할 것은 이미 모두 알고 있다. 이렇게 복잡한 일 중의 하나를 하려면, 그냥 평범한 파이썬을 추가해 조합하면 된다.

2.1   포장 함수

기억하자. 장식 함수는 임의의 함수를 돌려줄 수 있다. 그것을 포장 함수라고 부르겠다. 그 이유는 잠시 후에 밝혀진다. 여기에서 트릭은 포장 함수를 장식 함수 안에 정의해, 목표 함수를 포함하여 장식 함수의 변수 영역에 접근하도록 해주는 것이다.
# 's
 1def decorator(target):
 2
 3    def wrapper():
 4    print 'Calling function "%s"' % target.__name__
 5        return target()
 6
 7    # 포장 함수가 목표 함수를 교체하기 때문에, 속성을 목표 함수에 할당해 봐야 작동하지 않는다.
 8    # *포장 함수*에 할당할 필요가 있다.
 9    wrapper.attribute = 1
10    return wrapper
11
12@decorator
13def target():
14    print 'I am the target function'
15
16>>> target()
17Calling function "target"
18I am the target function
19
20>>> target.attribute
211
보시다시피, 포장 함수는 목표 함수의 반환 값을 돌려주는 간단한 사례를 포함하여 목표 함수에 무엇이든 할 수 있다. 그러나 목표 함수에 건네진 인자에 무슨 일이 일어나는가?

2.2   인자를 얻기

반환된 포장자 함수는 목표 함수를 교체하기 때문에, 포장 함수는 목표 함수에 건네어질 인자를 받을 것이다. 장식자가 어떤 목표 함수에도 작동하기를 원한다고 가정하면, 포장 함수는 그러면 임의의 비-키워드 인자와 임의의 키워드 인자를 받아야 한다. 필요하면 목표 함수에 인자를 건네고, 추가하고, 삭제하거나 또는 인자를 수정한다.
# 's
 1def decorator(target):
 2
 3    def wrapper(*args, **kwargs):
 4        kwargs.update({'debug': True}) # 여기에서 키워드 인자를 편집한다, 디버그 모드 활성화
 5        tempStr =  'Calling function "%s" with arguments %s and keyword arguments %s'
 5        print tempStr % (target.__name__, args, kwargs)
 6        return target(*args, **kwargs)
 7
 8    wrapper.attribute = 1
 9    return wrapper
10
11@decorator
12def target(a, b, debug=False):
13    if debug: print '[Debug] I am the target function'
14    return a+b
15
16>>> target(1,2)
17Calling function "target" with arguments (1, 2) and keyword arguments {'debug': True}
18[Debug] I am the target function
193
20
21>>> target.attribute
221
주의
장식자를 클래스 메쏘드에 적용할 수도 있다. 장식자가 언제나 이런 식으로 사용되고, 그 현재 실체에 접근할 필요가 있다면, 포장 함수는 첫 인자가 언제나self라고 간주할 수 있다:
# 's
1def wrapper(self, *args, **kwargs):
2    # 'self'로 일을 한다.
3    print self
4    return target(self, *args, **kwargs)

2.3   요약

그래서, 장식자 함수안에 정의된 임의의 인자를 받는 포장자 함수가 있다. 그 포장 함수는 원할 경우 목표 함수를 호출할 수 있다. 그 결과를 얻고, 그것으로 일을 한 다음, 원하는 것을 무엇이든 돌려줄 수 있다.
실행되기 전에 긍정적인 확인을 요구하는 특정한 함수 호출이 필요하다고 생각해 보자. 그 함수의 결과를 돌려주기 전에 문자열 처리를 하고 싶다고 해 보자. 내장 함수raw_input은 메시지를 인쇄한 다음 표준입력으로부터 응답을 기다린다.
# 's
 1def decorator(target):  # 파이썬은 목표 함수를 장식자에 건넨다.
 2
 3    def wrapper(*args, **kwargs):
 4        tempStr = 'Are you sure you want to call the function "%s"? '
 5        choice = raw_input( tempStr % target.__name__)
 6
 7        if choice and choice[0].lower() == 'y':
 8            # 입력이 'y'로 시작하면, 그 함수를 인자를 가지고 호출한다.
 9            result = target(*args, **kwargs)
10            return str(result)
11
12        else:
13            print 'Call to %s cancelled' % target.__name__
14
15    return wrapper
16
17@decorator
18def target(a,b):
19    return a+b
20
21>>> test.target(1,2)
22Are you sure you want to call the function "target"? n
23Call to target cancelled
24
25>>> test.target(1,2)
26Are you sure you want to call the function "target"? y
27'3'

3   동적인 장식자

종종 임의의 옵션을 장식자 함수에 건네 행위를 재단하고 싶을 경우가 있다. 장식자 구문을 대충 살펴보면 그렇게 할 방법이 없는 듯 보인다. 그냥 그런 장식자 아이디어를 완전히 버릴 수도 있지만, 그럴 필요가 없다.
해결책은 또다른 함수 안에 장식자 함수를 정의하는 것이다 -- 이를 옵션 함수라고 부르자. 목표 함수 정의 바로 앞에서, 즉 보통 (앞에 @를 붙인) 장식자 함수를 두던 곳에서 (앞에다 @를 붙여서) 대신 이 옵션 함수를 호출하자. 옵션 함수는 그러면 장식자 함수를 돌려주는데, 여기에다 파이썬은 전과 같이 목표 함수를 인자로 건넨다.

3.1   장식자에 옵션 건네기

옵션 함수는 얼마든지 인자를 받을 수 있다. 장식자 함수가 옵션 함수 안에 정의되어 있기 때문에, 장식자 함수는 옵션 함수에 건네진 인자에 얼마든지 접근한다.
# 's
 1def options(value):
 2
 3    def decorator(target):
 4        # 목표 함수로 여기에서 일을 한다.
 5        target.attribute = value
 6        return target
 7    return decorator
 8
 9@options('value')
10def target(a,b):
11    return a + b
12
13>>> target(1,2)
143
15
16>>> target.attribute
17'value'
보시다시피, 여기에서 장식자 구문 자체는 아무것도 바뀌지 않았다. 장식자 함수는 정적 영역이 아니라 동적 영역에 있다.

3.2   실행-시간 변형

앞에서와 같이 장식자 함수에서 포장 함수를 돌려주면 실행-시간 변형을 할 수 있다. 그렇지만 좋든 나쁘든 이제 세 가지 수준의 함수가 있다:
# 's
 1def options(debug_level):
 2
 3    def decorator(target):
 4
 5        def wrapper(*args, **kwargs):
 6            kwargs.update({'debug_level': debug_level})
 7            # 키워드 인자를 편집한다 여기에서, 디버그 수준을 옵션에 지정된 값으로 설정한다.
 8            tempStr = 'Calling function "%s" with arguments %s and keyword arguments %s'
 9            print tempStr % (target.__name__, args, kwargs)
10            return target(*args, **kwargs)
11
12        return wrapper
13
14    return decorator
15
16@options(5)
17def target(a, b, debug_level=0):
18    if debug_level: print '[Debug Level %s] I am the target function' % debug_level
19    return a+b
20
21>>> target(1,2)
22Calling function "target" with arguments (1, 2) and keyword arguments {'debug_level': 5}
23[Debug Level 5] I am the target function
243

4   약점: 함수 시그너처

휴. 이제 장식자로 무엇을 할 수 있는지 낱낱이 이해되는가? 좋다 :). 그렇지만, 꼭 언급해야 할 한 가지 약점이 있다.
장식자 함수가 돌려준 함수는 -- 보통 포장 함수 -- 목표 함수를 완전히 교체한다. 나중에 목표 함수라고 생각되는 것을 들여다 보면 실제로는 포장 함수를 들여다 보는 것이다.
대부분의 시간 동안은 문제가 없다. 일반적으로 몇 가지 옵션을 가지고 그냥 호출하면 된다. 프로그램은 그 함수의 __name__이 무엇인지 어떤 인자를 받는지 점검하지 않는다. 그래서 보통 이 문제는 문제가 아니다.
그렇지만 종종 호출할 함수가 특정 옵션을 지원하는지, 임의의 옵션 또는 그의 __name__이라는 것을 지원하는지 신경써야 할 경우가 있다. 또는 함수의 속성에 관심이 있을 수도 있다. 장식된 함수를 살펴보면, 실제로는 포장 함수를 보고 있는 셈이다.
아래의 예제에서 inspect 모듈의 getargspec 함수가 함수의 이름과 인자의 기본값을 얻는 것에 주목하자.
# 's
 1# 이 함수는 이름만 빼고 'target' 함수와 똑 같다.
 2def standalone_function(a,b):
 3    return a+b
 4
 5def decorator(target):
 6
 7    def wrapper(*args, **kwargs):
 8        return target()
 9
10    return wrapper
11
12@decorator
13def target(a,b):
14    return a+b
15
16>>> from inspect import getargspec
17
18>>> standalone_function.__name__
19'standalone_function'
20
21>>> getargspec(standalone_function)
22(['a', 'b'], None, None, None)
23
24>>> target.__name__
25'wrapper'
26
27>>> getargspec(target)
28([], 'args', 'kwargs', None)
보시다시피, 포장 함수는 원래 목표 함수와 다른 인자를 받았다고 보고한다. 그의 호출 시그너쳐가 바뀌었다.

4.1   해결책

이는 쉽게 해결할 수 있는 문제가 아니다. functools 모듈의 update_wrapper 메쏘드가 부분적인 해결책을 제공하는데, 이 메쏘드는 __name__과 기타 속성들을 한 함수에서 또다른 함수로 복사한다. 그러나 가장 큰 문제는 해결하지 못한다: 바뀐 호출 시그너쳐는 어떻게 할 것인가.
decorator 모듈의 decorator 함수가 가장 좋은 해결책을 제공한다: 이 함수는 포장 함수를 동적으로-평가되는 함수 안에 올바른 인자를 가지고 싸 넣을 수 있어서, 원래의 호출 시그너쳐를 복구해 준다. update_wrapper 함수와 비슷하게, 목표 함수의 __name__과 기타 속성을 가지고 포장 함수를 갱신할 수도 있다.
주의
앞으로 이 섹션에서 decorator 함수를 언급하면, 이 모듈에서 가져온 것을 의미한다. 목표 함수를 변형하는데 사용해 온 장식자 함수가 아님에 주의하자.
장식자를 만드는 또다른 방법
그렇지만 불행하게도 decorator 함수는 이를 염두에 두고 작성되지 않았다. 대신 나홀로 포장 함수를 완전히-성장한 장식자로 바꾸도록 작성되었다. 위의 실행-시간 변형에서 기술한 함수 내포에 관하여 걱정할 필요없이 말이다.
이 테크닉이 종종 유용하기는 하지만, 그 만큼 맞춤 재단하기가 힘들다. 함수가 실행될 때마다 무엇이든 실행시간에 처리된다목표 함수나 포장 함수 속성을 장식자에 할당하거나 또는 옵션을 장식자에 건넬 경우를 포함하여 목표 함수가 정의될 때는 아무 것도 할 수 없다.
또한, 생각건대 약간의 블랙 박스 같은 느낌이 든다; 약간 난잡하더라도 장식자가 무슨 일을 하는지 알면 좋겠다.
그러나 이 문제를 해결할 수 있다.
이상적으로 그냥 decorator(wrapper)를 호출하고 그냥 처리되도록 놔두면 된다. 그렇지만, 무슨 일이든 생각만큼 그렇게 간단한 것은 없다. 위에서 기술한 바와 같이, decorator 함수는 건네어진 함수를 -- 포장 함수를 -- 동적인 함수 안에 싸 넣어서 시그너처를 고친다. 그러나 여전히 몇 가지 문제가 있다:
문제 #1:
그 동적인 함수는 포장 함수를 (func, *args, **kwargs)로 호출한다.
해결책 #1:
포장 함수가 (func, *args, **kwargs)를 받도록 만든다. 그냥 (*args, **kwargs)를 받는 대신에 말이다.
문제 #2:
그 동적인 함수는 다음으로 실제 장식자로 사용될 또다른 함수 안에 포장된다 -- 목표 함수를 가지고 호출되어, 그 포장자 함수를 돌려줄 것이다.
해결책 #2:
목표 함수를 가지고 decorator의 반환 값을 호출하여 그 동적인 함수로 다시 돌아간다. 이 함수가 가진 시그너쳐가 올바르기 때문이다.
이 테크닉은 약간 해킹적이다. 설명하기가 좀 어렵지만 구현하기도 쉽고 잘 작동한다.
다음은 앞과 같은 예제이지만, 이제는 decorator 함수로 구현하였다 (그리고 이름이 바뀌어서 그렇게 혼란스럽지 않다):
# 's
 1from decorator import decorator
 2
 3def my_decorator(target):
 4
 5    def wrapper(target, *args, **kwargs): # target 함수 뒤에 인자 리스트가 온다.
 6        return target(*args, **kwargs)
 7
 8    # target 함수로 반환값을 호출하여 '적절한' 포장자 함수를 돌려받는다.
 9    return decorator(wrapper)(target)
10
11
12@my_decorator
13def target(a,b):
14    return a+b
15
16>>> from inspect import getargspec
17
18>>> target.__name__
19'target'
20
21>>> getargspec(target)
22(['a', 'b'], None, None, None)

5   모두 하나로 합치기

종종, 진짜로 해석-시간과 실행-시간에 모두 작동하는, 그리고 원래 목표 함수의 시그너쳐를 가진 맞춤재단 가능한 장식자가 필요한 경우가 있다.
다음은 모든 것을 하나로 묶는 예제이다. 앞의 예제를 확대하면, 다시 말해 실행되기 전에 긍정 확인을 요구하는 특정 함수 호출이 필요하면, 그리고 목표 함수마다 그 긍정 문자열을 맞춤재단하기를 바란다면. 게다가, 어떤 이유로 [1]목표 함수에 일치하는 그 장식된 함수의 시그너쳐가 필요하다.
자 시작해 보자:
# 's
 1from decorator import decorator
 2
 3# '옵션' 함수.  옵션을 받아 장식자를 돌려준다.
 4def confirm(text):
 5    '''
 6    전송될 문자열을 확인 메시지로 건넨다.  장식자를 돌려준다.
 7    '''
 8
 9    # 실제 장식자.  목표 함수를 받는다.
10    def my_decorator(target):
11        # 목표 함수가 최초에 해석될 때는 포장 함수 안에서 아무일도 일어나지 않는다.
12
13        # 장식 함수가 속성을 포장 함수에 복사하기 때문에 문제가 없다.
14        target.attribute = 1
15
16        # 포장 함수. 목표 함수를 교체하고 그의 인자들을 받는다.
17        def wrapper(target, *args, **kwargs):
18            # 여기에서 args나 kwargs를 가지고 일을 할 수 있다.
19
20            choice = raw_input(text)
21
22            if choice and choice[0].lower() == 'y':
23                # 입력이 'y'로 시작하면, 그 함수를 인자들을 가지고 호출한다
24                result = target(*args, **kwargs)
25                # 여기에서 그 결과를 가지고 일을 할 수 있다
26                return result
27
28            else:
29                print 'Call to %s cancelled' % target.__name__
30
31        # 포장자의 호출 시그너쳐를 고친다.
32        return decorator(wrapper)(target)
33
34    return my_decorator
35
36@confirm('Are you sure you want to add these numbers? ')
37def target(a,b):
38    return a+b
39
40>>> Are you sure you want to add these numbers? yes
413
42
43>>> target.attribute
441
성공이다. 실제로 작동한다.

6   맺는 말

언제나와 같이, 여기에서 언급한 것보다 더 좋은 방법이 있다면, 또는 혹 빼먹은 것이 있다면 자유롭게 이 글을 편집해서 그 문제를 고치셔도 좋다.

7   References

Michele Simionato's 'decorator' Module
Python Tips, Tricks, and Hacks - Decorators
[1]예를 들어, Pylons은 메쏘드 시그너쳐를 점검하여 어떤 인자를 건넬지 결정한다.





출처 : http://coreapython.hosting.paran.com/tiphack/Python%20Decorators%20Don't%20Have%20to%20be%20(that)%20Scary%20-%20Siafoo.htm#references

Articles