IT/언어

[python] 데코레이터(decorator)를 이해하기 위한 12단계 스텝

개발자 두더지 2021. 4. 7. 15:13
728x90

Step1. 함수


>>> def foo():
...     return 1
...
>>> foo()
1

 제일 기본이다. Python에 있어서 함수는 def 키워드로 함수명과 파라미터의 리스트(임의)를 이용해 정의한다. 또한 괄호를 붙인 이름을 지정하여 함수를 지정할 수 있다.

 

 

Step2. 스코프


 Python에서는 함수를 만들면 새로은 스코프가 만들어진다.  다시 말하자면 각각의 함수르 각각의 이름 공간을 가지고 있다는 의미이다.

 Python에서 이것을 확인하기 위한 함수도 준비되어 있다. locals()라는 함수로 자신이 가진 로컬 이름 공간의 값을 사전형으로 반환한다.

>>> def foo(arg):
...     x = 10
...     print locals()
...
>>> foo(20)
{'x': 10, 'arg': 20}

 또한 글로벌 이름 공간에 대해서는 globals() 함수로 확인가능하다.

>>> y = 30
>>> globals()
{..., 'y': 30} #Python이 자동적으로 만들 글로벌 변수도 표시되지만 생략

 

 

Step3. 변수의 해결 규칙


Python에서의 변수의 해결 룰은 다음과 같다.

① 작성할 때는 항상 새로운 변수가 그 이름공간 안에 만들어진다.

② 참고는 먼저 이름공간 내부터 검색하고 없으면 외부로 검색 영역을 넓혀간다.

함수의 로컬 스코프에서 글로벌 스코프의 변수를 참고해보자.

>>> text = "I am global!"
>>> def foo():
...     print text #1
...

>>> foo()
I am global!

 함수의 외부에 정의한 text의 내용이 표시되었다. #1부분은 먼저 함수 내의 로컬 변수를 검색하고 없으면 같은 text라는 이름의 글로벌 변수를 찾는다.

 그럼 이번에는 함수의 외부에 정의한 변수를 함수내에서 변경해보자.

>>> text = "I am global!"
>>> def foo():
...     text = "I am local!" #2
...     print locals()
...
>>> foo()
{'text': 'I am local!'}
>>> text
'I am global!'

 foo()가 호출될 때는 text의 내용으로써 함수내에 대입된 값이 셋팅되지만, 외부의 글로벌 함수의 text의 값은 바뀌지 않는다. #2에서는 실제 글로벌 변수를 찾으러 가지 않고 함수 foo내의 새로운 로컬 변수 text가 만들어진 것이다. 

 즉 함수의 안쪽에서는 글로벌 변수에 참고할 수 있는 것을 대입할 수 없다는 결론이 된다.

 

 

Step4. 변수의 라이프사이클


스코프뿐만 아니라 변수의 라이프 사이클에 대해서 알고 있어야할 필요가 있다.

>>> def foo():
...     x = 1
...
>>> foo()
>>> print x #1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

 여기서 에러가 발생하는 이유에 대해 스코프만으로는 알 수 없다. 네임스페이스는 함수 foo가 호출될 때마다 생성되며 처리가 끝나면 사라져버린다. 즉 위의 예에서 #1의 타이밍에서는 문자대로 x라는 이름의 변수가 존재하지 않는다는 의미이다.

 

 

Step5. 함수의 인수와 파라미터


 Python에서는 함수에 인수를 전달하는 것이 가능하다. 정의할 때의 파라미터의 이름은 로컬 변수의 이름을 로컬 변수의 이름으로써 사용된다.

>>> def foo(x):
...     print locals()
>>> foo(1)
{'x': 1} # 파라미터x가 로컬 변수명으로써 사용되고 있다.

 Python에서는 함수에 파라미터를 설정하거나 호출할 때에 인수를 전달하는 다양한 방법을 제공하고 있다. 파라미터에는 반드시 파라미터와 임의의 기본 파라미터가 있다.

>>> def foo(x, y=0): # 1
...     return x - y
>>> foo(3, 1) # 2
2
>>> foo(3) # 3
3
>>> foo() # 4
Traceback (most recent call last):
  ...
TypeError: foo() takes at least 1 argument (0 given)
>>> foo(y=1, x=3) # 5
2

 #1의 예에서는 x가 파라미터, 기본값을 지정하고 있는 y가 디폴트 파라미터가 된다. #2가 보통의 함수 사용 예이지만, #3과 같은 디폴트 파라미터에 대해서는 생략하는 것도 가능하다. 생략된 인수는 기본값(여기서는 0)이 된다. 

 또한 Python에 있어서는 이름을 붙인 인수도 사용할 수 있으므로 이름을 지정하여 순서를 신경스지 않고 인수를 지정하는 것도 같다(#5). Python에서는 인수는 단순 사전형이라고 이해하면 납득이 될 것이다.

 

 

Step6. 함수의 중첩


 Python에서는 함수 내에 다시 함수를 정의 즉 중첩할 수 있다.

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     inner() # 2
...
>>> outer()
1

 룰은 지금까지 봐왔던 것과 동일하다. 즉 #1에서는 로컬 변수 x를 찾아보지만 없으믈 외부의 네임 스페이스를 찾아서 outer내에 정의되어 있는 x를 참고한다. 또한 #2에서는 inner()을 호출하고 있지만, 여기서 중요한 것은 inner이라는 것도 하나의 변수명에 지나지 않고, 해결 규칙 내용을 바탕으로 outer내에 이름 공간 내에 정의를 찾아서 호출한다는 점이다.

 

 

Step7. 함수는 Python에 있어서 퍼스트 클래스 오브젝트이다.


 Python에서는 함수는 객체에 불과하다.

>>> issubclass(int, object) # Python내의 모든 객체는 같은 공통 클래스를 상속하여 만들어진다.
True
>>> def foo():
...     pass
>>> foo.__class__
<type 'function'>
>>> issubclass(foo.__class__, object)
True

 이 말이 어떤 말을 의미하냐면, 함수를 일반적으로 다른 변수와 동일하게 취급한다는 것이다. 즉, 다른 함수의 인수로 전달하거나 함수의 리턴값으로써 사용할 수 있다는 것이다. 아래의 샘플 코드에서 살펴보자.

>>> def add(x, y):
...     return x + y
>>> def sub(x, y):
...     return x - y
>>> def apply(func, x, y):
...     return func(x, y)
>>> apply(add, 2, 1)
3
>>> apply(sub, 2, 1)
1

 위의 예에서는 함수 apply는 파라미터로써 지정된 함수의 실행 결과를 리턴하도록 되어있어 함수 add, sub가 인수로써 ( 다른 변수와 다르지 않게 ) 전달되고 있음을 알 수 있다. 

 다음 예를 살펴보자.

>>> def outer():
...     def inner():
...         print "Inside inner"
...     return inner # 1
...
>>> foo = outer() #2
>>> foo
<function inner at 0x...>
>>> foo()
Inside inner

 위에서 살펴 봤던 코드와 다른 점은 #1에서 리턴값으로써 실행 결과를 보여주는 것이 아닌 함수 그 자체를 지정하고 있다는 것이다(inner()이 아닌 inner을 전달하고 있다).

 이것은 #2 와 같이 보통 대입가능하므로 foo에 함수를 넣어 실행하는 것이 가능하다는 것을 알 수 있다.

 

 

Step8. 클로저


 위에서 봤던 예를 살짝 변경해서 살펴보자.

>>> def outer():
...     x = 1
...     def inner():
...         print x
...     return inner
>>> foo = outer()
>>> foo.func_closure
(<cell at 0x...: int object at 0x...>,)

 inner는 outer에 의해 반환되는 함수로, foo에 저장되어 foo()에 의해 실행된다....는 말이 사실일까? Python의 변수 해결 규칙을 완전히 따르고 있는 말이지만, 라이프 사이클은 어떻게 되어 있을까? 변수 x는 outer함수가 실행되는 동안에만 존재한다. 여기서 outer함수는의 처리가 종료된 후에 inner함수가 foo에 대입하고 있으므로 foo()는 실행할 수 없는 것이 아닌가?

 그 예상과는 반대로 foo()는 실행가능하다. Python이 Function closure(클로저)의 기능을 가지고 있기 때문이다. 클로저는 글로벌 스코프 이외에 정의된 함수(예시의 경우에는 inner)이 "정의했을 때"의 자신을 포함한 스코프 정보를 기억하고 있는 것이다.  실제로 기억하고 있는가를 확인하기 위해 위와 같이 func_closure 프로퍼티를 호출하여 확인할 수 있다.

 "정의했을 때"라고 말한 것을 기억해두길 바란다. inner함수는 outer함수가 호출 될 때마다 새롭게 정의된다.

>>> def outer(x):
...     def inner():
...         print x
...     return inner
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2

 위의 예에서는 print1이나 print2에 직접 값을 인수로써 넣지 않아도 각각의 내부의 inner함수가 어떤 값을 출력해야하나를 기억하고 있다. 이것을 이용해서 고정 인수를 얻도록 커스터마이즈한 함수를 생성하는 것도 가능하다.

 

 

Step9. 데코레이터


 드디어 데코레이터에 관련된 이야기를 할 때가 왔다. 여기까지의 이야기를 바탕으로 결론부터 말하자면, 데코레이터란 "함수를 인수로 얻고 대가로 새로운 함수로 돌려주는 cllable(*)"이다.

>>> def outer(some_func):
...     def inner():
...         print "before some_func"
...         ret = some_func() #1
...         return ret + 1
...     return inner
>>> def foo():
...     return 1
>>> decorated = outer(foo) #2
>>> decorated()
before some_func
2

 하나 하나 이해해보도록 하자. 여기서 파라미터로써 some_func를 취득하는 outer이라는 함수를 정의하고 있다. 여기서 outer 안에 inner이라는 내부 함수가 정의되어 있다.

 inner은 문자열을 print한 후에, #1을 반환하는 값을 취득하고 있다. some_func는 outer를 호출하는 것으로 다른 값을 얻을 수 있지만, 여기서는 그것이 무엇이 되었든 아무튼 일단 실행 (call)하고 그 결과에 1을 더한 값을 반환한다. 마지막으로 outer함수는 inner함수의 것을 반환한다.

 #2에서 some_func로써 foo를 outer를 실행한 리턴값을 변수 decorated에 저장하고 있다. foo를 실행하면 1이 반환되지만 outer에을 씌운 foo는 그것에 +1을 하여 2를 반환하고 있다. 말하자면 decorated는 foo의 데코레이터판(foo + 어떠한 처리)라고 할 수 있다.

 실제로 유용한 데코레이터를 사용할 때에는 decorated와 같이 다른 변수를 준비할 필요없이 foo에 그대로 대입하는 경우가 많다. 즉 아래와 같다는 것이다.

foo = outer(foo)

 또 이전의 foo는 부르지 않고, 보통 데코레이트된 foo가 반환되는 것이 된다. 실용적인 예를 살펴보자.

 어떠한 좌표의 객체를 유지하는 라이브러리를 사용하고자한다. 이 객체는 x와 y의 좌표 쌍을 가지고 있지만, 아쉽게도 덧셈이나 나눗셈 등 수학 처리 기능은 없다. 그러나 우리가 이 라이브러리를 이용해서 대량의 계산 처리를 할 필요가 있어 라이브러리의 소스를 개편해야하는 반갑지 않은 상황이다. 

 우선 접근법으로는 아래와 같이 add, sub와 같은 함수를 만들면 좋을 것이라고 생각한다.

>>> class Coordinate(object):
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def __repr__(self):
...         return "Coord: " + str(self.__dict__)
>>> def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)
>>> def sub(a, b):
...     return Coordinate(a.x - b.x, a.y - b.y)
>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> add(one, two)
Coord: {'y': 400, 'x': 400}

 여기서 예를 들어 [취급할 좌표계는 0이하일 필요가 있다]라는 체크 처리가 필요하다면 어떻게 할 수 있을까? 즉 아래와 같은 상황이 발생을 막아야한다면 어떻게 코드를 작성할 수 있을까?

>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> three = Coordinate(-100, -100)
>>> sub(one, two)
Coord: {'y': 0, 'x': -200}
>>> add(one, three)
Coord: {'y': 100, 'x': 0}

 sub(one, two)는 (0, 0)을, add(one, three)는 (100, 200)을 반환하길 바라는 것이다. 각각의 함수에 하한을 체크하는 처리를 작성하는 것을 생각해볼 수 있겠지만, 여기서 데코레이터를 사용하여 체크 처리를 한 번에 처리하도록 만들어보자.

>>> def wrapper(func):
...     def checker(a, b):
...         if a.x < 0 or a.y < 0:
...             a = Coordinate(a.x if a.x > 0 else 0, a.y if a.y > 0 else 0)
...         if b.x < 0 or b.y < 0:
...             b = Coordinate(b.x if b.x > 0 else 0, b.y if b.y > 0 else 0)
...         ret = func(a, b)
...         if ret.x < 0 or ret.y < 0:
...             ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y > 0 else 0)
...         return ret
...     return checker
>>> add = wrapper(add)
>>> sub = wrapper(sub)
>>> sub(one, two)
Coord: {'y': 0, 'x': 0}
>>> add(one, three)
Coord: {'y': 200, 'x': 100}

 이전의 foo = outer(foo) 부분은 동일하지만 유용한 체크 구조를 파라미터와 함수의 처리 결과에 대해 적용할 수 있다. 

 

 

Step10. @심볼의 적용


Python에서는 데코레이터의 기재에 관해서 @기호를 사용한다. 즉 아래의 코드를

>>> add = wrapper(add)

이와 같은 형식으로 작성할 수 있다.

>>> @wrapper
... def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)

여기까지 읽어봤을 때, classmethod나 staticmethod와 같이 유용한 데코레이터를 자신이 만드는 것은 꽤 레벨이 높지만 적어도 데코레이터를 사용하는 것은 그렇게 어렵지 않다. 단지 @decoratorname를 기재할 뿐이다. 

 

 

Step11. *args와 **kwargs


 바로 위에서 설명한 데코레이터 wrapper은 유용하지만 파라미터가 2개뿐인 함수에만 적용할 수 있다. 물론 그건 그것대로 괜찮지만 더욱 유연한 함수에 적용할 수 있도록 데코레이터를 작성하고 싶은 경우는 어떻게 하는 것이 좋을까?

 Pytho에는 이것을 지원하는 기능이 준비되어 있다. 상세한 내용을 공식 사이트의 문서를 읽으면 좋겠지만, 함수를 정의할 때는 파라미터에 *(아스터리스크)를 붙이면 임의의 수의 필수 파라미터를 수용하도록 할 수 있다.

>>> def one(*args):
...     print args
>>> one()
()
>>> one(1, 2, 3)
(1, 2, 3)
>>> def two(x, y, *args):
...     print x, y, args
>>> two('a', 'b', 'c')
a b ('c',)

 임의의 파라미터 부분과 관련해서 리스트를 전달하고 있다는 것을 알 수 있다. 또한 정의할 때뿐만 아니라 호출할 때에도 인수에 *를 붙이면 기존 리스트나 튜플 형식의 인수를 언패키징해서 고종 인수에 적용해준다(아래의 코드를 참고). 

>>> def add(x, y):
...     return x + y
>>> lst = [1,2]
>>> add(lst[0], lst[1]) #1
3
>>> add(*lst) #2 <- # #1과 완전히 같은 의미이다.
3

 또한 **(애스터리스크 2개) 기법도 존재한다. 여기서는 리스트가 아닌 사전형이 된다.

>>> def foo(**kwargs):
...     print kwargs
>>> foo()
{}
>>> foo(x=1, y=2)
{'y': 2, 'x': 1}

 **kwargs를 함수 정의에 사용하는 것은 "명시적으로 지정하지 않은 파라미터는 kwargs이라는 이름의 사전으로 저장된다"는 의미이다. *args와 동일하게 함수를 호출할 때의 언패키징에도 대응한다.

>>> dct = {'x': 1, 'y': 2}
>>> def bar(x, y):
...     return x + y
>>> bar(**dct)
3

 

 

Step12. 제네릭한 데코레이터


위의 기능을 이용하여 함수의 인수를 로그에 출력해주는 데코레이터를 작성해보자. 간략화하기 위해 로그 출력은 stdout에 print하자.

>>> def logger(func):
...     def inner(*args, **kwargs): #1
...         print "Arguments were: %s, %s" % (args, kwargs)
...         return func(*args, **kwargs) #2
...     return inner

 #1에서 inner함수는 임의의 개수, 형식의 파라미터를 취득하는 것이 가능하여, #2에서 그것을 언패키징하여 인수로써 전달할 수 있다는 것을 주목하자. 이것에 의해 어떠한 함수에 대해서도 데코레이터 logger을 적용할 수 있다.

>>> @logger
... def foo1(x, y=1):
...     return x * y
>>> @logger
... def foo2():
...     return 2
>>> foo1(5, 4)
Arguments were: (5, 4), {}
20
>>> foo1(1)
Arguments were: (1,), {}
1
>>> foo2()
Arguments were: (), {}
2

 

 

더욱 자세히 알아보고 싶은사람을 위해


 마지막의 예까지 이해했다면, 데코레이터의 기초에 대해 이해했다고 할 수 있다. 잘 모르겠다면 그 전 단계로 돌아가 다시 하나 하나 차분히 살펴보길 바란다. 혹시 데코레이터에 대해서 더욱 학습하고 싶은 경우 아래의 두 사이트를 추천한다. (영어) 

Decorators I: Introduction to Python Decorators

Python Decorators II: Decorator Arguments


참고자료

qiita.com/_rdtr/items/d3bc1a8d4b7eb375c368

728x90

'IT > 언어' 카테고리의 다른 글

[python] os.path.join사용법  (0) 2021.05.11
[python] 타입힌트(Type Hints)  (0) 2021.04.12
[C#] C#의 Delegate  (0) 2021.03.14
[C#] C#의 Dictionary (사전형) 데이터 사용법  (0) 2021.03.06
[C#] C# 기본 문법  (0) 2021.02.16