0x00 装饰器(decorator)介绍 可以查看官方文档 对其的介绍:
返回值为另一个函数的函数,通常使用 @wrapper
语法形式来进行函数变换。 装饰器的常见例子包括 classmethod() 和 staticmethod() 。
装饰器语法只是一种语法糖,以下两个函数定义在语义上完全等价:
1 2 3 4 5 6 7 def f (... ): ... f = staticmethod (f) @staticmethod def f (... ): ...
同样的概念也适用于类,但通常较少这样使用。有关装饰器的详情可参见 函数定义 和 类定义 的文档。
python装饰器简单来说就是用一切皆对象和代码复用的思想去修改一个可调用对象的功能的函数,以达到让代码更加简洁的效果。这个功能也体现了python简洁 高效
的设计哲学。
0x01 一切皆对象 python中的函数是一个函数对象. python内存空间的存储特点:python右值对应一个内存空间,而变量名为这片内存的引用(用C++来理解的话,右值为对象,左值为这个对象的引用)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 In [1 ]: def foo (): ...: "this is test function" ...: return "Hello world" ...: In [2 ]: print (foo()) Hello world In [3 ]: print (foo) <function foo at 0x7f7528a135b0 > In [4 ]: a = foo In [5 ]: print (a()) Hello world In [6 ]: print (a) <function foo at 0x7f7528a135b0 > In [7 ]: del foo In [8 ]: print (foo) --------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input -8 -699c1a78ee01> in <module> ----> 1 print (foo) NameError: name 'foo' is not defined In [9 ]: print (a()) Hello world
0x02 闭包的概念 由于python语法的灵活性,可以在一个函数A中的函数体中在定义另一个函数B,其实就是在函数体内创建了一个函数对象,理所当然的可以将这个函数对象返回出去,而闭包就是在个这函数B的函数体中使用了函数A中的变量并且函数A最后把函数B返回了出去。 看看下面简单的例子就是一个闭包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 In [10 ]: def A (x ): ...: def B (y ): ...: return x+y ...: return B ...: ...: In [11 ]: print (f"{A} \n{A(1 )} " ) <function A at 0x7f7528136200 > <function A.<locals >.B at 0x7f75281353f0 > In [12 ]: print (A(1 )(2 )) 3 In [13 ]: C = A(5 ) In [14 ]: for i in range (3 ): ...: print (C(i)) ...: 5 6 7
0x03 装饰器的简单使用 查看一下官方文档中对于在函数定义中使用decorator的描述:
一个函数定义可以被一个或多个 decorator 表达式所包装。 当函数被定义时将在包含该函数定义的作用域中对装饰器表达式求值。 求值结果必须是一个可调用对象,它会以该函数对象作为唯一参数被发起调用。 其返回值将被绑定到函数名称而非函数对象。 多个装饰器会以嵌套方式被应用。 例如以下代码
1 2 3 @f1(arg ) @f2 def func (): pass
大致等价于
1 2 def func (): pass func = f1(arg)(f2(func))
不同之处在于原始函数并不会被临时绑定到名称 func
。
总接一下就是:
@
后面的变量的右值必须是一个可调用对象
作为decorator的函数对象(要被@
的对象)有且仅有一个参数,被修饰的函数(被@变量
修饰)就无所谓了。
e.g. 简单使用一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 In [1 ]: def decorator (fn ): ...: print ("start...." ) ...: fn() ...: print ("end...." ) ...: return 1 ...: In [2 ]: @decorator ...: def foo (): ...: print ("hello world" ) ...: start.... hello world end.... In [3 ]: print (f"{type (foo)} -[{foo} ] ---value {foo} " ) <class 'int '>-[1] ---value 1 In [4]: def decorator (fn ): ...: def addsomething (): ...: print ("start...." ) ...: fn() ...: print ("end...." ) ...: return addsomething ...: In [5 ]: @decorator ...: def foo (): ...: print ("hello world" ) ...: In [7 ]: print (f"{type (foo)} -[{foo} ]" ) <class 'function '>-[<function decorator .<locals >.addsomething at 0x7fe074357c70 >] In [8]: foo() start.... hello world end....
所以说修饰器本质就是将多个要动态重用的代码封装到一个装饰器中来让python解释器给一个被修饰的函数加上这些重用的代码。
但这时可能有dio大的要问了要是被修饰的函数需要参数怎么办? 很简单,在修饰器闭包的函数定义的参数中加上不定长参数*args, **kwargs
, 这样就可以传然后参数调用了,具体可以见官方文档 , 这里就不展开讲了。看下面的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 In [9 ]: def decorator (fn ): ...: def addsomething (*args, **kwargs ): ...: print ("start...." ) ...: fn(*args, **kwargs) ...: print ("end...." ) ...: return addsomething ...: In [10 ]: @decorator ...: def foo (name, country="CN" ): ...: print (f"I'm {name} , from {country} " ) ...: ...: In [11 ]: foo() start.... --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input -11 -c19b6d9633cf> in <module> ----> 1 foo() <ipython-input -9 -03a692c62082> in addsomething(*args, **kwargs) 2 def addsomething (*args, **kwargs ): 3 print ("start...." ) ----> 4 fn(*args, **kwargs) 5 print ("end...." ) 6 return addsomething TypeError: foo() missing 1 required positional argument: 'name' In [12 ]: foo("wakaka" ) start.... I'm wakaka, from CN end....
0x04 一些问题 可能有些细心的朋友发现了,在上面的使用方式中,经过修饰器修饰过后的函数已经变成了修饰器内的闭包函数,即函数名和注释文档都变成了别人的形状,这好吗?这不好,纯爱战士狂怒,那么不想被替换怎么办?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 In [9 ]: def decorator (fn ): ...: def addsomething (*args, **kwargs ): ...: print ("start...." ) ...: fn(*args, **kwargs) ...: print ("end...." ) ...: return addsomething ...: In [18 ]: def foo (name: str , country: str = 'CN' ) -> None : ...: """ ...: this is simple test demo ...: """ ...: print (f"I'm {name} , from {country} " ) ...: In [19 ]: print (f"{foo} \n\tdocument: {foo.__doc__} \n\t annotations: {foo.__annotations__} " ) <function foo at 0x7fe07425e050 > document: this is simple test demo annotations: {'name' : <class 'str '>, 'country ': <class 'str '>, 'return ': None } In [20 ]: @decorator ...: def foo (name: str , country: str = 'CN' ) -> None : ...: """ ...: this is simple test demo ...: """ ...: print (f"I'm {name} , from {country} " ) ...: ...: In [21 ]: print (f"{foo} \n\tdocument: {foo.__doc__} \n\t annotations: {foo.__annotations__} " ) <function decorator.<locals >.addsomething at 0x7fe06d93bf40 > document: None annotations: {}
幸好的是,python给我们提供了functools.wraps
函数来解决这个问题, 查看官方文档 . 修改上面例子来使用它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 In [22 ]: from functools import wraps In [23 ]: def decorator (fn ): ...: @wraps(fn) ...: def addsomething (*args, **kwargs ): ...: print ("start...." ) ...: fn(*args, **kwargs) ...: print ("end...." ) ...: return addsomething ...: In [24 ]: @decorator ...: def foo (name: str , country: str = 'CN' ) -> None : ...: """ ...: this is simple test demo ...: """ ...: print (f"I'm {name} , from {country} " ) ...: ...: In [25 ]: print (f"{foo} \n\twhoami: {foo.__name__} \n\tdocument: {foo.__doc__} \n\t annotations: {foo.__annotations__} " ) <function foo at 0x7fe06da4d1b0 > whoami: foo document: this is simple test demo annotations: {'name' : <class 'str '>, 'country ': <class 'str '>, 'return ': None }
这里懒得去看源码了,可以简单的推测一下functools.wraps
装饰器内部是将fn函数的一些属性(如__name__、__annotations__、__doc__、...
)取出来然后修改闭包函数的属性然后在修饰函数的时候就达到效果了
0x05 带参数的修饰器 这里说的带参数的修饰器指的@
后面的变量传参了,如@functools.wraps(fn)
, 但其@
后面的变量本质还是一个可调用对象. 在我看来这种方式像有一个修饰器工厂一样,根据传入的材料不同而去制作不同的修饰器 下面来使用一下这种方式 e.g. 用修饰器来制定日志输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 In [2 ]: from functools import wraps In [7 ]: def logout (logfile='test.log' ): ...: def log_decorator (fn ): ...: from time import ctime ...: @wraps(fn) ...: def logging (*args, **kwargs ): ...: msg = f"[{ctime()} ] {fn.__name__} start...." ...: res = fn(*args, **kwargs) ...: msg += f"\n[{ctime()} ] {fn.__name__} end...." ...: print (msg) ...: ...: with open (logfile, 'a' ) as f: ...: f.write(msg+'\n' ) ...: return res ...: return logging ...: return log_decorator ...: In [8 ]: @logout() ...: def foo (): ...: pass ...: In [9 ]: @logout('foo2.log' ) ...: def foo2 (): ...: pass ...: In [10 ]: foo() [Mon Jan 17 18 :52 :55 2022 ] foo start.... [Mon Jan 17 18 :52 :55 2022 ] foo end.... In [11 ]: foo2() [Mon Jan 17 18 :52 :59 2022 ] foo2 start.... [Mon Jan 17 18 :52 :59 2022 ] foo2 end....
查看一下当前目录下的文件
1 2 3 decorator.md foo2.log test.log
没毛病(^_^)
0x06 修饰器类 修饰器也可以修饰类,这种用法和修饰函数差不多,参考官方文档 . 但本节要说的不是这个,而是利用python的魔术方法__call__
实现一个修饰器的类.__call__
是函数调用其实可以代表()
, 是函数调用的时候用的, 举个简单的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 In [1 ]: def foo (): ...: print ("hi" ) ...: In [2 ]: foo() hi In [3 ]: foo.__call__() hi In [4 ]: class A : ...: def __call__ (self ): ...: print ("hello" ) ...: In [5 ]: a = A() In [6 ]: a() hello In [7 ]: A.__call__(a) hello In [8 ]: a.__call__() hello
将0x05的例子用修饰器类的方式改写一下 e.g. 直接调用方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from time import ctimeclass logging (object ): log_file = 'test.log' def __init__ (self, func ): self.func = func self.msg_s = "[{0}] {1} start...\n" self.msg_e = "[{0}] {1} end...\n" def __call__ (self, *args, **kwars ): msg = self.msg_s.format (ctime(), self.func.__name__) res = self.func(*args, **kwars) msg += self.msg_e.format (ctime(), self.func.__name__) print (msg) with open (self.log_file, "a" ) as f: f.write(msg) return res @logging def foo (): pass @logging def foo2 (): pass if __name__ == '__main__' : print (f"{type (foo)} {foo} " ) foo() logging.log_file = 'foo2.log' foo2()
运行结果
1 2 3 4 5 6 <class '__main__.logging'> <__main__.logging object at 0x7fa03da8bfd0> [Mon Jan 17 20:14:36 2022] foo start... [Mon Jan 17 20:14:36 2022] foo end... [Mon Jan 17 20:14:36 2022] foo2 start... [Mon Jan 17 20:14:36 2022] foo2 end...
可以看见被修饰后的函数变成了logging类的实例方法, 且这种方式使用functools.wraps
的修饰方式也差不多, 只要理解了decorator的本质, 这里的结果就不难理解.
e.g. 带参数方式的修饰器类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 from time import ctimefrom functools import wrapsclass logout (object ): def __init__ (self, logfile = 'test.log' ): self.logfile = logfile self.msg_s = "[{0}] {1} start...\n" self.msg_e = "[{0}] {1} end...\n" def __call__ (self, func ): @wraps(func ) def logging (*args, **kwargs ): msg = self.msg_s.format (ctime(), func.__name__) res = func(*args, **kwargs) msg += self.msg_e.format (ctime(), func.__name__) print (msg) with open (self.logfile, "a" ) as f: f.write(msg) return res return logging @logout() def foo (): pass @logout('foo2.log' ) def foo2 (): pass if __name__ == '__main__' : print (f"{type (foo)} {foo} " ) foo() foo2()
运行结果:
1 2 3 4 5 6 <class 'function'> <function foo at 0x7fcd736c3f40> [Mon Jan 17 20:28:23 2022] foo start... [Mon Jan 17 20:28:23 2022] foo end... [Mon Jan 17 20:28:23 2022] foo2 start... [Mon Jan 17 20:28:23 2022] foo2 end...
可以发现修饰过后还是一个函数
RERERENCE [1] https://docs.python.org/zh-cn/3/ [2] https://zhuanlan.zhihu.com/p/87353829 [3] https://eastlakeside.gitbook.io/interpy-zh/decorators