Python의 데코레이터는 Python에 입문하는 데 장애물이 됩니다.
데코레이터가 필요한 이유
귀하의 프로그램이 두 가지 함수 say_hello()와 say_goodbye()를 구현한다고 가정합니다.
def say_hello(): print "hello!" def say_goodbye(): print "hello!" # bug here if __name__ == '__main__': say_hello() say_goodbye()
그러나 실제 호출에서 프로그램이 잘못되었음을 발견했으며 위 코드는 hello 두 개를 인쇄했습니다. 디버깅 후 say_goodbye()가 잘못되었음을 발견했습니다. 상사는 각 메소드를 호출하기 전에 입력 함수의 이름을 기록하도록 요구합니다. 예를 들면 다음과 같습니다.
[DEBUG]: Enter say_hello() Hello! [DEBUG]: Enter say_goodbye() Goodbye!
좋아요. A는 졸업생이고 이렇게 구현했습니다.
def say_hello(): print "[DEBUG]: enter say_hello()" print "hello!" def say_goodbye(): print "[DEBUG]: enter say_goodbye()" print "hello!" if __name__ == '__main__': say_hello() say_goodbye()
아주 낮죠? 꼬마 B가 한동안 일을 하다가 꼬마 A에게 이렇게 써도 된다고 하더군요.
def debug(): import inspect caller_name = inspect.stack()[1][3] print "[DEBUG]: enter {}()".format(caller_name) def say_hello(): debug() print "hello!" def say_goodbye(): debug() print "goodbye!" if __name__ == '__main__': say_hello() say_goodbye()
물론 좋겠지만, 비즈니스 함수마다 debug() 함수를 호출하는 것이 불편하다고 상사가 say 관련 함수에 대해서는 디버깅이 필요하지 않다고 하면 어떻게 될까요? -관련 기능이 필요한가요?
그럼 이때 데코레이터가 등장해야겠죠.
데코레이터는 본질적으로 Python 함수이므로 코드를 변경하지 않고도 다른 함수가 추가 함수를 추가할 수 있습니다. 데코레이터의 반환 값도 함수 개체입니다. 로그 삽입, 성능 테스트, 트랜잭션 처리, 캐싱, 권한 확인 등과 같은 교차 요구 사항이 있는 시나리오에서 자주 사용됩니다. 데코레이터는 이런 문제를 해결하기 위한 훌륭한 디자인입니다. 데코레이터를 사용하면 기능 자체와 관련이 없는 유사한 코드를 대량으로 추출하여 계속해서 재사용할 수 있습니다.
간단히 말해서 데코레이터의 역할은 기존 함수나 객체에 추가 기능을 추가하는 것입니다.
데코레이터 작성 방법
초기(Python 버전 < 2.4, 2004년 이전)에는 함수에 추가 기능을 추가하는 방법이 이랬습니다. < 2.4,2004年以前),为一个函数添加额外功能的写法是这样的。
def debug(func): def wrapper(): print "[DEBUG]: enter {}()".format(func.__name__) return func() return wrapper def say_hello(): print "hello!" say_hello = debug(say_hello) # 添加功能并保持原函数名不变
위의 디버그 함수는 실제로는 원래 함수를 래핑하고 몇 가지 추가 함수를 추가하여 다른 함수를 반환합니다. 이런 방식으로 작성하는 것은 그다지 우아하지 않기 때문에 Python의 이후 버전에서는 @ 구문 설탕을 지원합니다. 다음 코드는 이전 작성 방법과 동일합니다.
def debug(func): def wrapper(): print "[DEBUG]: enter {}()".format(func.__name__) return func() return wrapper @debug def say_hello(): print "hello!"
이것은 가장 간단한 데코레이터이지만, 데코레이팅된 함수가 매개변수를 전달해야 하는 경우 문제가 있습니다. 반환된 함수는 매개변수를 허용할 수 없으므로 데코레이터 함수 래퍼가 다음과 같이 원래 함수와 동일한 매개변수를 허용하도록 지정할 수 있습니다.
def debug(func): def wrapper(something): # 指定一毛一样的参数 print "[DEBUG]: enter {}()".format(func.__name__) return func(something) return wrapper # 返回包装过函数 @debug def say(something): print "hello {}!".format(something)
이런 방식으로 한 가지 문제를 해결하고 N개를 더 추가합니다. 문제. 수천 개의 함수가 있기 때문에 자신의 함수에만 관심이 있습니다. 다행히도 Python에서는 변수 매개 변수 *args 및 키워드 매개 변수 **kwargs를 사용할 수 있습니다. 모든 대상 함수
def debug(func): def wrapper(*args, **kwargs): # 指定宇宙无敌参数 print "[DEBUG]: enter {}()".format(func.__name__) print 'Prepare and say...', return func(*args, **kwargs) return wrapper # 返回 @debug def say(something): print "hello {}!".format(something)
이제 데코레이터 작성의 기본 방법을 완전히 마스터했습니다.
고급 데코레이터
매개변수가 있는 데코레이터와 클래스 데코레이터는 고급 콘텐츠입니다. 이러한 데코레이터를 이해하기 전에 함수 클로저와 데코레이터 인터페이스 규칙을 어느 정도 이해하는 것이 가장 좋습니다. (http://betacat.online/posts/p 참조...
매개변수가 있는 데코레이터
이전 데코레이터가 특정 로그 정보를 입력하는 것 이상의 작업을 완료해야 한다고 가정합니다. 함수이고 로그 수준을 지정해야 합니다. 그러면 데코레이터는 다음과 같습니다.
def logging(level): def wrapper(func): def inner_wrapper(*args, **kwargs): print "[{level}]: enter function {func}()".format( level=level, func=func.__name__) return func(*args, **kwargs) return inner_wrapper return wrapper @logging(level='INFO') def say(something): print "say {}!".format(something) # 如果没有使用@语法,等同于 # say = logging(level='INFO')(say) @logging(level='DEBUG') def do(something): print "do {}...".format(something) if __name__ == '__main__': say('hello') do("my work")
매개변수가 있는 데코레이터를 사용하면 이렇게 이해할 수 있습니다. @logging(level='DEBUG')와 같은 함수에 부딪히면 실제로는 함수이며 반환된 결과가 데코레이터인 한 문제가 없습니다.
클래스 기반 데코레이터
데코레이터 함수는 실제로 호출 가능한 객체를 매개변수로 받아들인 다음 Python에서 호출 가능한 객체를 반환해야 하는 인터페이스 제약 조건입니다. 그러나 예외가 있습니다. 객체가 __call__() 메서드를 오버로드하는 한 객체는 __call__처럼 앞뒤에 밑줄을 사용하여 호출할 수 있습니다. 메서드는 Python에서 내장 메서드라고 하며 때로는 이를 재정의하는 메서드라고도 합니다. 매직 메소드는 일반적으로 객체의 내부 동작을 변경합니다. 위의 예는 클래스 객체에 호출되는 동작을 제공합니다.
데코레이터의 개념으로 돌아가서 데코레이터는 호출 가능한 객체를 수신하고 호출 가능한 객체를 반환해야 합니다(너무 엄격하지는 않음). , 자세한 내용은 아래 참조), 클래스를 사용하여 구현할 수도 있습니다. 생성자 __init__()는 함수를 받아들인 다음 __call__()을 오버로드하고 함수를 반환합니다. 이 함수는 데코레이터 함수의 효과도 얻을 수 있습니다class Test(): def __call__(self): print 'call me!' t = Test() t() # call me
클래스를 통해 매개변수가 있는 데코레이터를 구현해야 하는 경우 이전 예제보다 조금 더 복잡해집니다. 그러면 생성자가 함수를 허용하지 않습니다. , 그러나 클래스를 통해 전달된 매개변수는 저장합니다. 그런 다음 __call__ 메서드를 오버로드할 때
class logging(object): def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): print "[DEBUG]: enter function {func}()".format( func=self.func.__name__) return self.func(*args, **kwargs) @logging def say(something): print "say {}!".format(something)
class logging(object): def __init__(self, level='INFO'): self.level = level def __call__(self, func): # 接受函数 def wrapper(*args, **kwargs): print "[{level}]: enter function {func}()".format( level=self.level, func=func.__name__) func(*args, **kwargs) return wrapper #返回函数 @logging(level='INFO') def say(something): print "say {}!".format(something)
@property
이해하기 전에. 이 데코레이터는 데코레이션 없이 어떻게 사용할 수 있는지 알아야 합니다
def getx(self): return self._x def setx(self, value): self._x = value def delx(self): del self._x # create a property x = property(getx, setx, delx, "I am doc for x property")
以上就是一个Python属性的标准写法,其实和Java挺像的,但是太罗嗦。有了@语法糖,能达到一样的效果但看起来更简单。
@property def x(self): ... # 等同于 def x(self): ... x = property(x)
属性有三个装饰器:setter, getter, deleter ,都是在property()的基础上做了一些封装,因为setter和deleter是property()的第二和第三个参数,不能直接套用@语法。getter装饰器和不带getter的属性装饰器效果是一样的,估计只是为了凑数,本身没有任何存在的意义。经过@property装饰过的函数返回的不再是一个函数,而是一个property对象。
>>> property() <property object at 0x10ff07940>
@staticmethod,@classmethod
有了@property装饰器的了解,这两个装饰器的原理是差不多的。@staticmethod返回的是一个staticmethod类对象,而@classmethod返回的是一个classmethod类对象。他们都是调用的是各自的__init__()构造函数。
class classmethod(object): """ classmethod(function) -> method """ def __init__(self, function): # for @classmethod decorator pass # ... class staticmethod(object): """ staticmethod(function) -> method """ def __init__(self, function): # for @staticmethod decorator pass # ...
装饰器的@语法就等同调用了这两个类的构造函数。
class Foo(object): @staticmethod def bar(): pass # 等同于 bar = staticmethod(bar)
至此,我们上文提到的装饰器接口定义可以更加明确一些,装饰器必须接受一个callable对象,其实它并不关心你返回什么,可以是另外一个callable对象(大部分情况),也可以是其他类对象,比如property。
装饰器里的那些坑
装饰器可以让你代码更加优雅,减少重复,但也不全是优点,也会带来一些问题。
位置错误的代码
让我们直接看示例代码。
def html_tags(tag_name): print 'begin outer function.' def wrapper_(func): print "begin of inner wrapper function." def wrapper(*args, **kwargs): content = func(*args, **kwargs) print "<{tag}>{content}</{tag}>".format(tag=tag_name, content=content) print 'end of inner wrapper function.' return wrapper print 'end of outer function' return wrapper_ @html_tags('b') def hello(name='Toby'): return 'Hello {}!'.format(name) hello() hello()
在装饰器中我在各个可能的位置都加上了print语句,用于记录被调用的情况。你知道他们最后打印出来的顺序吗?如果你心里没底,那么最好不要在装饰器函数之外添加逻辑功能,否则这个装饰器就不受你控制了。以下是输出结果:
begin outer function. end of outer function begin of inner wrapper function. end of inner wrapper function. <b>Hello Toby!</b> <b>Hello Toby!</b>
错误的函数签名和文档
装饰器装饰过的函数看上去名字没变,其实已经变了。
def logging(func): def wrapper(*args, **kwargs): """print log before a function.""" print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) return func(*args, **kwargs) return wrapper @logging def say(something): """say something""" print "say {}!".format(something) print say.__name__ # wrapper
为什么会这样呢?只要你想想装饰器的语法糖@代替的东西就明白了。@等同于这样的写法。
say = logging(say)
logging其实返回的函数名字刚好是wrapper,那么上面的这个语句刚好就是把这个结果赋值给say,say的__name__自然也就是wrapper了,不仅仅是name,其他属性也都是来自wrapper,比如doc,source等等。
使用标准库里的functools.wraps,可以基本解决这个问题。
from functools import wraps def logging(func): @wraps(func) def wrapper(*args, **kwargs): """print log before a function.""" print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) return func(*args, **kwargs) return wrapper @logging def say(something): """say something""" print "say {}!".format(something) print say.__name__ # say print say.__doc__ # say something
看上去不错!主要问题解决了,但其实还不太完美。因为函数的签名和源码还是拿不到的。
import inspect print inspect.getargspec(say) # failed print inspect.getsource(say) # failed
如果要彻底解决这个问题可以借用第三方包,比如wrapt。后文有介绍。
不能装饰@staticmethod 或者 @classmethod
当你想把装饰器用在一个静态方法或者类方法时,不好意思,报错了。
class Car(object): def __init__(self, model): self.model = model @logging # 装饰实例方法,OK def run(self): print "{} is running!".format(self.model) @logging # 装饰静态方法,Failed @staticmethod def check_model_for(obj): if isinstance(obj, Car): print "The model of your car is {}".format(obj.model) else: print "{} is not a car!".format(obj) """ Traceback (most recent call last): ... File "example_4.py", line 10, in logging @wraps(func) File "C:\Python27\lib\functools.py", line 33, in update_wrapper setattr(wrapper, attr, getattr(wrapped, attr)) AttributeError: 'staticmethod' object has no attribute '__module__' """
前面已经解释了@staticmethod这个装饰器,其实它返回的并不是一个callable对象,而是一个staticmethod对象,那么它是不符合装饰器要求的(比如传入一个callable对象),你自然不能在它之上再加别的装饰器。要解决这个问题很简单,只要把你的装饰器放在@staticmethod之前就好了,因为你的装饰器返回的还是一个正常的函数,然后再加上一个@staticmethod是不会出问题的。
class Car(object): def __init__(self, model): self.model = model @staticmethod @logging # 在@staticmethod之前装饰,OK def check_model_for(obj): pass
如何优化你的装饰器
嵌套的装饰函数不太直观,我们可以使用第三方包类改进这样的情况,让装饰器函数可读性更好。
decorator.py
decorator.py 是一个非常简单的装饰器加强包。你可以很直观的先定义包装函数wrapper(),再使用decorate(func, wrapper)方法就可以完成一个装饰器。
from decorator import decorate def wrapper(func, *args, **kwargs): """print log before a function.""" print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) return func(*args, **kwargs) def logging(func): return decorate(func, wrapper) # 用wrapper装饰func
你也可以使用它自带的@decorator装饰器来完成你的装饰器。
from decorator import decorator @decorator def logging(func, *args, **kwargs): print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) return func(*args, **kwargs)
decorator.py实现的装饰器能完整保留原函数的name,doc和args,唯一有问题的就是inspect.getsource(func)返回的还是装饰器的源代码,你需要改成inspect.getsource(func.__wrapped__)。
wrapt
wrapt是一个功能非常完善的包,用于实现各种你想到或者你没想到的装饰器。使用wrapt实现的装饰器你不需要担心之前inspect中遇到的所有问题,因为它都帮你处理了,甚至inspect.getsource(func)也准确无误。
import wrapt # without argument in decorator @wrapt.decorator def logging(wrapped, instance, args, kwargs): # instance is must print "[DEBUG]: enter {}()".format(wrapped.__name__) return wrapped(*args, **kwargs) @logging def say(something): pass
使用wrapt你只需要定义一个装饰器函数,但是函数签名是固定的,必须是(wrapped, instance, args, kwargs),注意第二个参数instance是必须的,就算你不用它。当装饰器装饰在不同位置时它将得到不同的值,比如装饰在类实例方法时你可以拿到这个类实例。根据instance的值你能够更加灵活的调整你的装饰器。另外,args和kwargs也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。
如果你需要使用wrapt写一个带参数的装饰器,可以这样写。
def logging(level): @wrapt.decorator def wrapper(wrapped, instance, args, kwargs): print "[{}]: enter {}()".format(level, wrapped.__name__) return wrapped(*args, **kwargs) return wrapper @logging(level="INFO") def do(work): pass
关于wrapt的使用,建议查阅官方文档,在此不在赘述。
http://wrapt.readthedocs.io/e...
小结
Python의 데코레이터는 Java의 주석과 동일하지 않으며 C#의 속성과도 동일하지 않습니다. 둘은 완전히 다른 개념입니다.
데코레이터의 개념은 본래의 기능과 객체를 강화하는 것인데, 이는 재캡슐화와 동일하므로 일반적으로 데코레이터 함수를 패키징이라는 의미로 래퍼()라고 부릅니다. 함수는 호출될 때만 해당 기능을 수행합니다. 예를 들어 @logging 데코레이터는 함수 실행 시 추가 로그를 출력할 수 있고, @cache로 데코레이션된 함수는 계산 결과 등을 캐시할 수 있습니다.
주석과 기능은 대상 함수나 객체에 일부 속성을 추가합니다. 이는 분류와 동일합니다. 이러한 속성은 리플렉션을 통해 얻을 수 있으며, 프로그램이 실행될 때 다양한 특성의 기능이나 객체가 개입될 수 있습니다. 예를 들어, Setup이 포함된 함수는 준비 단계로 실행되거나 TestMethod가 포함된 모든 함수가 순차적으로 검색되어 실행됩니다.
지금까지 제가 알고 있는 데코레이터에 대한 이야기는 마쳤지만, 데코레이터 클래스 데코레이터 등 아직 언급되지 않은 부분도 있습니다. 기회가 되면 더 추가하겠습니다. 시청해 주셔서 감사합니다.