Python装饰器的来龙去脉

xiaoxiao2021-02-28  58

本文转至 [余子越的博客](http://www.yuchaoshui.com/) ,文章 [Python 装饰器](http://www.yuchaoshui.com/post/Python-decorator),欢迎访问[yuchaoshui.com](http://yuchaoshui.com) 了解更多信息!

文章目录

一、装饰器二、装饰器实现1. 把函数传递到一个新函数获取2. 更改函数的调用方式3. 使用Python提供的语法糖 @ 三、双重装饰器四、保留函数元信息五、解除一个装饰器六、有参数的装饰器七、带可选参数的装饰器八、装饰器实现函数参数类型检查九、内置装饰器十、将装饰器定义在类内部十一、为类方法和静态方法提供装饰器十二、类装饰器

一、装饰器

  装饰器,顾名思义就是用来做装饰用的,Python中装饰器用来装饰函数,它的好处是不需要对原来的函数做任何修改就可以对函数进行装饰。

二、装饰器实现

  现在要获取函数 a() 的运行时间,可以编写一个装饰器来装饰,下面分三个步骤,一步一步引出装饰器的语法糖 @ 。

1. 把函数传递到一个新函数获取

import time def a(): time.sleep(1) print('in a()') def timeit(func): def wrapper(): start = int(time.time()) func() end =int(time.time()) print('used:{} second(s)'.format(end - start)) return wrapper timeit(a)()

  把函数作为参数传递到timeit(),在timeit()里面返回wrapper()函数,这个wrapper()的作用就是获取函数a()的运行时间。 在timeit(a)()中, timeit(a)获得的是一个函数,最后面的括号是函数的调用。   这种方法的缺点就是是改变了函数的调用方式,如果函数 a() 在很多地方都有,那么你需要在每个地方都修改一下调用方式才能获取到函数的运行时间。

2. 更改函数的调用方式

  要解决上面的问题,我们可以将 timeit(a) 重新赋值给 a , 这样就可以直接使用 a() 这样的方式来获取函数运行时间了。

import time def a(): time.sleep(1) print('in a()') def timeit(func): def wrapper(): start = int(time.time()) func() end =int(time.time()) print('used:{} second(s)'.format(end - start)) return wrapper a = timeit(a) a()

3. 使用Python提供的语法糖 @

   为了简洁至上,Python 已经帮我们用语法糖 @ 来实现了上面的赋值(a = timeit(a))那个步骤。

import time def timeit(func): def wrapper(): start = int(time.time()) func() end =int(time.time()) print('used:{} second(s)'.format(end - start)) return wrapper @timeit def a(): time.sleep(1) print('in a()') a()

   使用@timeit,在函数的定义处加上这一行,与另外写 a = timeit(a)完全等价,千万不要以为@有另外的魔力。除了字符输入少了一些,还有一个额外的好处就是这样看上去更有装饰器的感觉。

三、双重装饰器

  有时一个函数可能需要不止一个装饰器,多个装饰器要注意装饰器的顺序。

def makebold(fn): def wrapper(): return "<b>" + fn() + "</b>" return wrapper def makeitalic(fn): def wrapper(): return "<i>" + fn() + "</i>" return wrapper @makebold @makeitalic def say(): return("hello") print(say())

   @makeitalic装饰器的作用是把数据变斜体, @makebold 的作用是把数据变为粗体。你可以把它们两个调换一下位置看看有什么效果。

四、保留函数元信息

  当写了一个装饰器作用在某个函数上时,如果不做任何操作,这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了。任何时候定义装饰器的时候,都应该使用 functools 库中的 @wraps 装饰器来注解底层包装函数。

import time from functools import wraps def timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() func(*args, **kwargs) end = time.time() print(func.__name__, end-start) return wrapper @timethis def myfun(): ''' myfun's doc! ''' print("original function!") time.sleep(2) myfun() print(myfun.__name__) print(myfun.__doc__)

五、解除一个装饰器

  当想撤销已装饰的函数时,可以通过访问 __wrapped__ 属性来访问原始的未包装的那个函数。

import time from functools import wraps def timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() func(*args, **kwargs) end = time.time() print(func.__name__, end-start) return wrapper @timethis def myfun(): ''' myfun's doc! ''' print("original function!") time.sleep(2) myfun = myfun.__wrapped__ myfun() 这里的方法仅仅适用于在包装器中正确使用了 @wraps 的情况。如果有多个装饰器,那么访问 __wrapped__ 属性的行为是不可预知的,应该避免这种行为。内置的装饰器 @staticmethod 和 @classmethod 无法使用 __wrapped__ 属性,因为这两个装饰器没有使用 @wraps 装饰。

六、有参数的装饰器

   当装饰器有参数时,比如下面的 delay 装饰器。

import time def delay(seconds): def new_deco(func): time.sleep(seconds) print(func) return func return new_deco @delay(2) def func_one(): print("func_one() , do something!") func_one()

  第一个函数 delay 是装饰函数,它的参数是用来 加强装饰 的。由于此函数并非被装饰的函数对象,所以在内部必须至少创建一个接受被装饰函数的函数,然后返回这个对象。实际上此时相当于 func_one = delay(2)(func_one)。

七、带可选参数的装饰器

  一个装饰器,有时不需要传递参数(@logged),有时需要传递参数(@logged(level=logging.DEBUG))。为了实现这种这种调用方式,装饰器参数可以使用 带默认值的命名关键字参数 来实现。

from functools import wraps, partial import logging def logged(func=None, *, level='DEBUG'): if func is None: return partial(logged, level=level) @wraps(func) def wrapper(*args, **kwargs): print('log level: {0}'.format(level)) return func(*args, **kwargs) return wrapper @logged def add(x, y): return x + y @logged(level='INFO') def plus(x, y): return x + y print(add(3,4)) print() print(plus(3,4))

  结果如下:

log level: DEBUG 7 log level: INFO 7

  可以看到,@logged 装饰器可以同时不带参数(注意调用时没有括号)或带参数。这样做仅仅是一种编程习惯。

  对于无参数的装饰器:

@logged def add(x, y): return x + y

  等价于

def add(x, y): return x + y add = logged(add)

  这时候,被装饰函数会被当做第一个参数直接传递给 logged 装饰器。 因此,logged() 中的第一个参数就是被包装函数本身。所有其他参数都必须有默认值。

  对于有参数的装饰器: ``` @logged(level='INFO') def plus(x, y): return x + y ```   等价于 ``` def plus(x, y): return x + y plus = logged(level='INFO')(plus) ```   这时候,初始调用 `logged()` 函数时,被包装函数并没有传递进来。 因此在装饰器内,它必须是可选的,使得其他参数必须使用关键字来指定。   然后装饰器要返回一个接受一个函数参数并包装它的函数。 可以使用 `functools.partial` 。 它会返回一个未完全初始化的自身,除了被包装函数外其他参数都已经确定下来了。在返回的 `logged`函数里面,再传入唯一未确定的参数 **被装饰函数**。

八、装饰器实现函数参数类型检查

  在函数定义处使用装饰器 @typeassert 即可对函数的参数做类型限制,看下面的例子。

from inspect import signature from functools import wraps def typeassert(*ty_args, **ty_kwargs): def decorate(func): # If in optimized mode, disable type checking if not __debug__: return func # Map function argument names to supplied types sig = signature(func) bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments @wraps(func) def wrapper(*args, **kwargs): bound_values = sig.bind(*args, **kwargs) # Enforce type assertions across supplied arguments for name, value in bound_values.arguments.items(): if name in bound_types: if not isinstance(value, bound_types[name]): raise TypeError( 'Argument {} must be {}'.format(name, bound_types[name]) ) return func(*args, **kwargs) return wrapper return decorate @typeassert(int, list, z=dict) def spam(x, y, z={'age':20}): print(x, y, z) spam(1, 2, 3) spam(1, [2,3], {'age':23})

九、内置装饰器

  内置的装饰器中常用的有三个,分别是staticmethod、classmethod、property,作用分别是把类中定义的实例方法变成静态方法、类方法、类属性。这几个内置装饰器在  Python 面向对象详解  一文中有详细讲解。

十、将装饰器定义在类内部

  将我们定义的一些装饰器进行分类管理,将功能相似的装饰器放在一个类里面。 定义是需要注意的是:装饰器是作为一个实例方法还是类方法。

from functools import wraps class Dec: def decorator1(self, func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 1') return func(*args, **kwargs) return wrapper @classmethod def decorator2(cls, func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 2') return func(*args, **kwargs) return wrapper d = Dec() @d.decorator1 def foo(): pass @Dec.decorator2 def bar(): pass foo() bar()

  上面的例子中,一个是实例调用,一个是类调用。在类中定义装饰器这种方法,在标准库中有很多这样的例子。 比如 @property 装饰器实际上是一个类,它里面定义了三个方法 getter(), setter(), deleter(), 每一个方法都是一个装饰器。

十一、为类方法和静态方法提供装饰器

  当给类方法或静态方法提供装饰器时。要确保装饰器在 @classmethod或 @staticmethod 之前。例如:

import time from functools import wraps def timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() r = func(*args, **kwargs) end = time.time() print(end-start) return r return wrapper class Spam: @timethis def instance_method(self, n): while n > 0: print(self, n) n -= 1 @classmethod @timethis def class_method(cls, n): while n > 0: print(cls, n) n -= 1 @staticmethod @timethis def static_method(n): while n > 0: print(n) n -= 1 s = Spam() s.instance_method(3) print() Spam.class_method(3) print() Spam.static_method(3)

  运行结果:

<__main__.Spam object at 0x7fc498657d68> 3 <__main__.Spam object at 0x7fc498657d68> 2 <__main__.Spam object at 0x7fc498657d68> 1 6.079673767089844e-05 <class '__main__.Spam'> 3 <class '__main__.Spam'> 2 <class '__main__.Spam'> 1 1.1444091796875e-05 3 2 1 7.62939453125e-06

十二、类装饰器

  类装饰器通常可以作为其他高级技术比如混入或元类的一种非常简洁的替代方案。

def log_getattribute(cls): ori_getattribute = cls.__getattribute__ def new_getattribute(self, name): print('getting:', name) return ori_getattribute(self, name) cls.__getattribute__ = new_getattribute return cls @log_getattribute class A: def __init__(self,x): self.x = x def spam(self): print(self.x) a = A(3) a.spam()

  上面的类装饰器可以用继承的方式实现

class LoggedGetattribute: def __getattribute__(self, name): print('getting:', name) return super().__getattribute__(name) class A(LoggedGetattribute): def __init__(self,x): self.x = x def spam(self): print(self.x) a = A(3) a.spam()

  某种程度上来讲,类装饰器方案显得更加直观,并且它不会引入新的继承体系。它的运行速度也更快一些, 因为他并不依赖 super() 函数。

本文转至 [余子越的博客](http://www.yuchaoshui.com/) ,文章 [Python 装饰器](http://www.yuchaoshui.com/post/Python-decorator),欢迎访问[yuchaoshui.com](http://yuchaoshui.com) 了解更多信息!
转载请注明原文地址: https://www.6miu.com/read-2099972.html

最新回复(0)