python decoreator를 이해하기 위해서는 퍼스트 클래스 함수와 클로저를 이해하고 있어야 합니다. 만약 이해하고 계시지 않으시다면 밑의 강좌를 참고하여 주십시오.
파이썬 – 퍼스트클래스 함수 (First Class Function)
파이썬 – 클로저 (Closure)
데코레이터란 무엇일까요? 사전적 의미로는 “장식가” 또는 “인테리어 디자이너” 등의 의미를 가지고 있습니다. 이름 그대로, 자신의 방을 예쁜 벽지나 커튼으로 장식을 하듯이, 기존의 코드에 여러가지 기능을 추가하는 파이썬 구문이라고 생각하시면 됩니다.
Step1. 함수
>>> def foo():
... return 1
...
>>> foo()
1
제일 기본이다. Python에 있어서 함수는 def 키워드로 함수명과 파라미터의 리스트(임의)를 이용해 정의한다. 또한 괄호를 붙인 이름을 지정하여 함수를 지정할 수 있다
Step2. 스코프
Python에서는 함수를 만들면 새로은 스코프가 만들어진다. 다시 말하자면 각각의 함수는 각각의 이름 공간을 가지고 있다는 의미이다.
Python에서 이것을 확인하기 위한 함수도 준비되어 있다. locals()라는 함수로 자신이 가진 로컬 이름 공간의 값을 dictionary 타입으로 반환한다.
locals() 함수는 현재의 local 변수를 dict type으로 던져주는 내장 함수입니다.
def foo(arg):
x = 10
print(locals())
foo(20)
## 실행결과
{'x': 10, 'arg': 20}
또한 글로벌 이름 공간에 대해서는 globals() 함수로 확인가능하다.
y = 30
print(globals())
## 실행결과
{..., 'y': 30}
함수는 Python에 있어서 퍼스트 클래스 오브젝트이다.
Python에서는 함수는 객체입니다.
print(issubclass(int, object)) # Python내의 모든 객체는 같은 공통 클래스를 상속하여 만들어진다.
def foo():
pass
print(issubclass(foo.__class__, object))
## 실행결과
True
True
위 코드의 의미는, 함수를 일반적으로 다른 변수와 동일하게 취급한다는 것입니다. 즉, 다른 함수의 인수로 전달하거나 함수의 리턴값으로써 사용할 수 있다는 것입니다.
def add(x, y):
return x + y
def sub(x, y):
return x - y
def apply(func, x, y):
return func(x, y)
print(apply(add, 2, 1))
print(apply(sub, 2, 1))
위의 예에서는 함수 apply는 파라미터로써 함수 add, sub를 인수로써 ( 다른 변수와 다르지 않게 ) 전달하고 있음을 알 수 있습니다.
def outer():
def inner():
print("Inside inner")
return inner # 1
foo = outer() #2
print(foo)
foo()
## 실행결과
<function outer.<locals>.inner at 0x000002BAEC7DEB00>
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
출처: https://engineer-mole.tistory.com/181 [매일 꾸준히, 더 깊이]
일반 데코레이터
- 이렇게 코딩하고 실행하면
# 얘가 데코레이터 def decorator(func): def decorator(*args, **kwargs): print("%s %s" % (func.__name__, "before")) result = func(*args, **kwargs) print("%s %s" % (func.__name__ , "after")) return result return decorator # 함수에 데코레이터를 붙여준다. @decorator def func(x, y): print(x + y) return x + y func(1,2)
- 이런 결과가 나온다. 아! 신통방통 하다!
func before 3 func after
- @데코레이터는 사실 이거랑 같은 의미라고 한다
def decorator(func): def decorator(*args, **kwargs): print("%s %s" % (func.__name__, "before")) result = func(*args, **kwargs) print("%s %s" % (func.__name__ , "after")) return result return decorator def func(x, y): print(x + y) return x + y func2 = decorator(func) func2(1,2)
파라메터를 가지는 데코레이터
- 데코레이터에 뭔가 파라메터를 전달하고 싶을데는 약간 복잡하긴 하지만 역시 다 된다! function을 감싸는 decorator를 다시 감싸주면 된다.
# 얘가 파라메터도 붙는 데코레이터 def decorator_with_param(param): def wrapper(func): def decorator(*args, **kwargs): print(param) print("%s %s" % (func.__name__, "before")) result = func(*args, **kwargs) print("%s %s" % (func.__name__ , "after")) return result return decorator return wrapper @decorator_with_param("hello, decorator!") def func(x, y): print(x + y) return x + y func(1,2)
- 결과는 이렇게 나온다!
hello, decorator! func before 3 func after
-------
본질적으로, decorators는 wrapper로써 동작하며, 원본 함수를 수정할 필요 없이 원본 함수의 실행 전/후의 동작을 변경할 수 있습니다
함수에 대해 알아야 할 것
깊게 들어가기 전에, 명확히 알아야 할 전제조건이 있습니다. python에서 함수는 일급시민이며, 일급객체입니다. 이 말은 아래에 나오는 여러 유용한 작업들을 할 수 있다는 의미입니다.
함수를 변수에 대입할 수 있다.
def greet(name):
return "hello "+name
greet_someone = greet
print greet_someone("John")
# Outputs: hello John
함수 내부에 다른 함수를 정의할 수 있다.
def greet(name):
def get_message():
return "Hello "
result = get_message()+name
return result
print(greet("John"))
## 실행결과
Hello John
함수는 다른 함수의 인자로 전달될 수 있다.
def greet(name):
return "Hello " + name
def call_func(func):
other_name = "John"
return func(other_name)
print(call_func(greet))
## 실행결과
Hello John
함수는 다른 함수에 의해 리턴될 수 있다.
다른 말로 함수는 다른 함수를 생성 할 수 있습니다.
def compose_greet_func():
def get_message():
return "Hello there!"
return get_message
greet = compose_greet_func()
print(greet())
## 실행결과
Hello there!
내부 함수는 둘러싸인 범위(enclosing scope)에 대해 접근이 가능하다.
보통 closure로 알려져 있습니다. 내부 함수의 근접한 scope로부터 name 인자를 읽기 위해 위의 예제로부터 어떻게 변경했는지 봅시다.
def compose_greet_func(name):
def get_message():
return "Hello there "+name+"!"
return get_message
greet = compose_greet_func("John")
print(greet())
## 실행결과
Hello there John!
Decorators의 구성
함수 decorators는 존재하는 함수에 대한 간단한 wrappers입니다. 위에서 언급한 아이디어를 모아서 decorators를 만들 수 있습니다. 아래 예제에서, p 태그에 의해 문자열을 출력하는 함수를 wrap 하는 것을 고려해보겠습니다.
def get_text(name):
return "lorem ipsum, {0} dolor sit amet".format(name)
def p_decorate(func):
def func_wrapper(name):
return "<p>{0}</p>".format(func(name))
return func_wrapper
my_get_text = p_decorate(get_text)
print my_get_text("John")
# <p>Outputs lorem ipsum, John dolor sit amet</p>
이게 우리의 첫 번째 decorator 입니다. 함수는 다른 함수를 인자로 전달 받고 기존 함수의 기능에 추가적인 작업을 하는 새로운 함수를 생성하고 리턴합니다. get_text 함수를 p_decorate로 꾸미고 싶다면 우린 단지 p_decorate의 리턴 값을 get_text에 할당하면 됩니다.
get_text = p_decorate(get_text)
print get_text("John")
# Outputs lorem ipsum, John dolor sit amet
Python’s Decorator 문법
python은 문법적인 sugar(컴퓨터 용어 sugar란 기존 기능을 좀 더 쉽게 쓰기 위한 문법적 방법)를 통해 programmer들에게 좀 더 명확하고 보기 좋은 decorator 방법을 제공합니다. get_text를 decorate 하기 위해 get_text = p_decorator(get_text)와 같은 코드를 쓸 필요가 없습니다. decorated 된 함수 전에 decoration 하는 함수의 이름을 언급하는 방법이 있습니다. decorator 이름과 함께 @ 심볼이 나와야 합니다.
def p_decorate(func):
def func_wrapper(name):
return "<p>{0}</p>".format(func(name))
return func_wrapper
@p_decorate
def get_text(name):
return "lorem ipsum, {0} dolor sit amet".format(name)
print get_text("John")
# Outputs <p>lorem ipsum, John dolor sit amet</p>
이제 우리는 get_text 함수를 div와 string 태그로 감싸는 두 개의 다른 함수를 고려해봅시다.
def p_decorate(func):
def func_wrapper(name):
return "<p>{0}</p>".format(func(name))
return func_wrapper
def strong_decorate(func):
def func_wrapper(name):
return "<strong>{0}</strong>".format(func(name))
return func_wrapper
def div_decorate(func):
def func_wrapper(name):
return "<div>{0}</div>".format(func(name))
return func_wrapper
기본적인 접근방법으론, 아래 코드와 같이 decorating 할 수 있습니다.
get_text = div_decorate(p_decorate(strong_decorate(get_text)))
Python의 decorator 문법으로 동일한 기능을 제공합니다.
@div_decorate
@p_decorate
@strong_decorate
def get_text(name):
return "lorem ipsum, {0} dolor sit amet".format(name)
print get_text("John")
# Outputs <div><p><strong>lorem ipsum, John dolor sit amet</strong></p></div>
여기서 한가지 중요한 것은 decorators의 순서입니다. 만약 위의 예제에서 decorators의 순서가 달라지면 결과도 달라집니다.
Class의 Method Decorating
Python에서 methods는 첫 번째 인자가 객체를 참조(self)하는 함수입니다. wrapper 함수의 인자를 self를 받는 방법으로 동일하게 methods에 대해 decorators를 만들 수 있습니다.
def p_decorate(func):
def func_wrapper(self):
return "<p>{0}</p>".format(func(self))
return func_wrapper
class Person(object):
def __init__(self):
self.name = "John"
self.family = "Doe"
@p_decorate
def get_fullname(self):
return self.name+" "+self.family
my_person = Person()
print my_person.get_fullname()
더 좋은 접근은 우리의 decorator가 함수와 methods 모두에게 유용하도록 만드는 것입니다. 이 방법을 위해 wrapper 함수의 인자로 args 와 *kwargs를 넣고 이를 통해 가변인자와 가변 키워드 인자를 전달받을 수 있습니다.
def p_decorate(func):
def func_wrapper(*args, **kwargs):
return "<p>{0}</p>".format(func(*args, **kwargs))
return func_wrapper
class Person(object):
def __init__(self):
self.name = "John"
self.family = "Doe"
@p_decorate
def get_fullname(self):
return self.name+" "+self.family
my_person = Person()
print my_person.get_fullname()
decorators에게 인자 전달하기
위의 예제들을 돌아보면, 많은 decorators 예제들이 중복되어 있는 것을 알 수 있습니다. 3개의 decorators( div_decorate, p_decorate, strong_decorate)를 살펴보면 name 변수의 string을 서로 다른 태그로 감싸는 것만 다르고 다른 동작은 모두 동일합니다. 우리는 명백하게 더 많은 것을 할 수 있어야만 합니다. tag를 인자로 받아 string을 감싸는 일반적인 구현방법은 없을까요? 제발염!
def tags(tag_name):
def tags_decorator(func):
def func_wrapper(name):
return "<{0}>{1}</{0}>".format(tag_name, func(name))
return func_wrapper
return tags_decorator
@tags("p")
def get_text(name):
return "Hello "+name
print get_text("John")
# Outputs <p>Hello John</p>
위 경우 조금 더 많은 작업을 했습니다. Decorators는 인자로 함수를 받기를 기대하기 때문에, 우리는 추가적인 인자를 받아 우리의 decorator를 즉석에서 만드는 함수를 정의했습니다. 위의 예제에선 tags로 우리의 decorator를 생성합니다.
decorated 된 함수 디버깅
결국 가장 중요한 것은 decorators는 단지 우리가 만든 함수를 wrapping 한다는 것입니다. 이 파트에선, wrapper 함수가 원본 함수의 name, module, docstring을 가지지 않기 때문에 발생하는 문제를 다룹니다. 위의 예제들을 기반으로 할 경우 :
print get_text.__name__
# Outputs func_wrapper
출력은 get_text로 기대했지만, get_text의 __name__, __doc__, __module__의 속성이 이런 warpper 함수들에 의해 오버라이딩 됩니다. 분명하게 우리는 wrapper 함수 안에서 이들을 reset 해줘야 하고, python은 훨씬 좋은 방법을 제공합니다.
Functools 이 대안입니다.
운좋게도 python (version 2.5)는 functools.wraps를 포함하는 functools 모듈을 가지고 있습니다. Wraps는 wrapping 하는 함수의 속성들을 원본 함수의 속성들로 update 해주는 decorator 입니다. 아래 예제가 있습니다.
from functools import wraps
def tags(tag_name):
def tags_decorator(func):
@wraps(func)
def func_wrapper(name):
return "<{0}>{1}</{0}>".format(tag_name, func(name))
return func_wrapper
return tags_decorator
@tags("p")
def get_text(name):
"""returns some text"""
return "Hello "+name
print get_text.__name__ # get_text
print get_text.__doc__ # returns some text
print get_text.__module__ # __main__
이제 get_text의 속성이 제대로 나오는 것을 알 수 있습니다.
decorators는 어디에 쓰나?
decorators 는 우리의 프로그램을 더욱 더 강력하고 우아하게 만들어줍니다. 일반적으로 decorators는 우리가 함수의 기능변경을 원치 않는 상황에서 기능을 확장하고 싶을 때 사용하는 개념입니다.
'Python' 카테고리의 다른 글
Python - module (0) | 2022.01.06 |
---|---|
Python - class (0) | 2022.01.06 |
파이썬 - list (0) | 2022.01.05 |
파이썬 - lambda 함수 (0) | 2022.01.05 |
Python 코드를 실행하는 Visual Studio Code에서 code runner 메시지를 숨기는 방법 (0) | 2022.01.04 |