装饰器的作用
打印日志: @log
检测性能:@performance
数据库事务: @transaction
URL路由:@post(‘/register’)
装饰器的目的:
使代码可读性更高,感觉高大上;
代码结构更加清晰,代码冗余度更低;
无参数decorator Python的 decorator 本质上就是一个高阶函数,它接收一个函数作为参数,然后,返回一个新函数。
使用 decorator 用Python提供的 @ 语法,这样可以避免手动编写f = decorate(f) 这样的代码。
考察一个@log的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def testlog (func ): def inner (arg ): print ("print log:" + func.__name__) func(arg) return inner def test (x ): print ("this is test function:" ,x) test = testlog(test) test("11" ) @testlog def test (x ): print ("this is test function:" ,x) test("11" )
例子:阶乘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from functools import reducedef testlog (func ): def inner (*args,**kwargs ): print ("print log:" + func.__name__) func(*args,**kwargs) return inner @testlog def factorial (x, y ): print ("this is factorial function:" ) res = reduce(lambda x, y: x * y, range (x, y)) print (res) return res factorial(1 , 11 )
例子:写一个@performance,作为一个函数计时器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import timedef performance (func ): def inner (*args, **kwargs ): time1 = time.time() res = func(*args, **kwargs) time2 = time.time() print (f"call %s%s%s in %fs" % (func.__name__, args,kwargs, time2 - time1)) return res return inner @performance def factorial (x, y ): return reduce(lambda x, y: x * y, range (x, y)) print (factorial(1 , 11 ))
有参数decorator 在之前没有带参数的装饰器上添加参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from functools import reduceimport timedef performance (unit ): def decorator_ (func ): def wrap (*args, **kwargs ): time1 = time.time() res = func(*args, **kwargs) time.sleep(1 ) time2 = time.time() t = 1000 * (time2 - time1) if unit == 'ms' else (time2 - time1) print (f"call %s in %s %s" %(func.__name__, t, unit)) return res return wrap return decorator_ @performance('ms' ) def factorial_ (n ): return reduce(lambda x,y: x*y, range (1 , n+1 )) print (factorial_(6 ))
装饰器的优化 @decorator 可以动态实现函数功能的增加,但是,经过@decorator“改造”后的函数,和原函数相比,除了功能多一点外,还有其它不同的地方。
在没有decorator的情况下,打印函数名:
1 2 3 def f1 (x ): pass print (f1.__name__)
有decorator的情况下,再打印函数名:
1 2 3 4 5 6 7 8 def log (func ): def wrapper (*args, **kw ): return func(*args, **kw) return wrapper @log def f2 (x ): pass print (f2.__name__)
由于decorator返回的新函数函数名已经不是’f2’,而是@log内部定义的’wrapper’。这对于那些依赖函数名的代码就会失效。decorator还改变了函数的 __doc__ 等其它属性。如果要让调用者看不出一个函数经过了@decorator的“改造”,就需要把原函数的一些属性复制到新函数中:
1 2 3 4 5 6 7 8 9 10 def log (func ): def wrapper (*args, **kw ): return func(*args, **kw) wrapper.__name__ = func.__name__ wrapper.__doc__ = func.__doc__ return wrapper @log def f2 (x ): pass print (f2.__name__)
这样写decorator很不方便,也很难把原函数的所有必要属性都一个一个复制到新函数上,所以Python内置的 functools 可以用来自动化完成这个“复制”的任务:
1 2 3 4 5 6 7 8 9 10 import functoolsdef log (func ): @functools.wraps(func ) def wrapper (*args, **kw ): return func(*args, **kw) return wrapper @log def f2 (x ): pass print (f2.__name__)
1 2 3 4 5 6 7 8 9 10 11 12 13 def performance (prefix ): def log (func ): @functools.wraps(func ) def wrapper (*args, **kw ): return func(*args, **kw) return wrapper return log @performance("debug" ) def f2 (x ): pass print (f2.__name__)
类装饰器(不带参数) 基于类装饰器的实现,必须实现 __call__ 和 __init__两个内置函数。__init__ :接收被装饰函数__call__ :实现装饰逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class logger (object ): def __init__ (self, func ): self .func = func def __call__ (self, *args, **kwargs ): print (f"logger:{self.func.__name__} in decorator" ) return self .func(*args, **kwargs) @logger def test (para ): print (f"test func({para} )" ) test("123" )
类装饰器(带参数) 若还需要打印DEBUG WARNING等级别的日志。这就需要给类装饰器传入参数,给这个函数指定级别了。
带参数和不带参数的类装饰器有很大的不同。
__init__ :不再接收被装饰函数,而是接收传入参数。__call__ :接收被装饰函数,实现装饰逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class logger (object ): def __init__ (self, level='info' ): self .level = level def __call__ (self, func ): def wrapper (*args, **kwargs ): print (f"logger[{self.level} ]:{func.__name__} in decorator" ) func(*args, **kwargs) return wrapper @logger(level='warning' ) def test_funct (para ): print (f'test test_funct({para} )' ) test_funct("321" )
偏函数 当一个函数有很多参数时,调用者就需要提供多个参数。如果减少参数个数 ,就可以简化写代码的负担。
比如,int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:
但int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做 N 进制的转换:
1 2 3 4 >>> int ('12345' , base=8 )5349 >>> int ('12345' , 16 )74565
假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:
1 2 def int2 (x, base=2 ): return int (x, base)
这样,我们转换二进制就非常方便了:
1 2 3 4 >>> int2('1000000' )64 >>> int2('1010101' )85
functools.partial 就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:
1 2 3 4 5 6 >>> import functools>>> int2 = functools.partial(int , base=2 )>>> int2('1000000' )64 >>> int2("11" ,base=2 ) 3
所以,functools.partial 可以把一个多参数的函数变成一个参数少的新函数,少的参数需要在创建时指定默认值,这样,新函数调用的难度就降低了。
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 from functools import partialdef read_file (filename, mode='r' , encoding='utf-8' , errors='strict' ): with open (filename, mode=mode, encoding=encoding, errors=errors) as f: return f.read() read_log = partial(read_file, encoding='utf-8' , errors='ignore' ) def round_number (number, ndigits=0 , base=10 ): """按指定进位基数四舍五入""" return round (number / base) * base round_to_tens = partial(round_number, base=10 ) round_to_hundreds = partial(round_number, base=100 ) print (round_to_tens(126 )) print (round_to_hundreds(126 ))
偏函数与类实现装饰器 大多数装饰器都是基于函数和闭包实现的,但这并非构造装饰器的唯一方式。
Python 对某个对象是否能通过装饰器( @decorator)形式使用只有一个要求:decorator 必须是一个可被调用(callable)的对象 。
对于callable 对象,最熟悉的就是函数了;
除函数之外,类也可以是 callable 对象,只要实现了__call__ 函数(上面几个例子)。
还有容易被人忽略的偏函数其实也是 callable 对象。
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 import timeimport functoolsclass DelayFunc : def __init__ (self, duration, func ): self .duration = duration self .func = func def __call__ (self, *args, **kwargs ): print (f'Wait for {self.duration} seconds...' ) time.sleep(self .duration) return self .func(*args, **kwargs) def eager_call (self, *args, **kwargs ): print ('Call without delay' ) return self .func(*args, **kwargs) def delay (duration ): """ 装饰器:推迟某个函数的执行。 同时提供 .eager_call 方法立即执行 """ return functools.partial(DelayFunc, duration) @delay(duration=2 ) def add (a, b ): return a+b print (add) print (add(3 , 5 )) print (add.eager_call(1 ,2 ))
装饰类的装饰器 用 Python 写单例模式 的时候,常用的有三种写法。其中一种,是用装饰器来实现的。
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 instances = {} def singleton (cls ): def get_instance (*args, **kwargs ): cls_name = cls.__name__ print ("cls_name:" ,cls_name) if not cls_name in instances: print (f"cle_name[{cls_name} ] not in instances" ) instance = cls(*args, **kwargs) instances[cls_name] = instance return instances[cls_name] return get_instance @singleton class User : _instance = None def __init__ (self, name ): self .name = name print ("name=" ,self .name) u1 =User("usr1" ) print (u1,u1.name)u2 =User("usr2" ) print (u2,u2.name)print (u1 == u2)
wraps再理解 了解完偏函数后再回看 functools.wraps() 的作用
wraps 其实是一个偏函数对象(partial),源码如下:
1 2 3 4 5 def wraps (wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES ): return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
wraps其实就是调用了一个函数update_wrapper,知道原理后,我们改写上面的代码,在不使用 wraps的情况下,也可以让wrapped.__name__ 打印出 wrapped,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from functools import update_wrapperWRAPPER_ASSIGNMENTS = ('__module__' , '__name__' , '__qualname__' , '__doc__' , '__annotations__' ) def decorator (func ): def inner (): pass update_wrapper(inner, func, assigned=WRAPPER_ASSIGNMENTS) return inner @decorator def testfunc (): pass print (testfunc.__name__)
一般最常使用的就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 from functools import wrapsdef decorator (func ): @wraps(func ) def inner (): pass return inner @decorator def testfunc (): pass print (testfunc.__name__)
python内置装饰器:property property通常存在于类中,可以将一个函数定义成一个属性,属性的值就是该函数return的内容。
初学Python时这样给实例绑定属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Person (object ): def __init__ (self, name, age= None ): self .name = name self .age = age p1 = Person("person1" ) p1.age = 25 print (p1.name)del p1.age
但是这样存在问题:直接把属性暴露出去 ,虽然写起来很简单,但是并不能对属性的值做合法性限制。合理的写法如下:
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 class Person (object ): def __init__ (self, name, age= None ): self .name = name self .__age = age def set_age (self, age ): if not isinstance (age, int ): raise ValueError('input illegal: age must be int.' ) if not 0 < age < 100 : raise ValueError('input illegal: age must between 0 and 150.' ) self .__age = age def get_age (self ): return self .__age def del_age (self ): self .__age = None p1 = Person("tiny" ) p1.set_age(25 ) print (p1.get_age())p1.del_age()
上面的代码设计虽然可以变量的定义,但是可以发现不管是获取还是赋值(通过函数)都和我们平时想要的的不同。我们想要的是这样形式的:赋值p1.age = 25, 获取p1.age。
使用@property :
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 class Person (object ): def __init__ (self, name, age=None ): self .name = name self .__age = age @property def age (self ): return self .__age @age.setter def age (self, value ): if not isinstance (value, int ): raise ValueError('age must be int.' ) if not 0 < value < 150 : raise ValueError('age must between 1 and 150.' ) self .__age = value @age.deleter def age (self ): del self .__age p1 = Person("tiny" ) p1.age = 25 print (p1.age)del p1.age
用@property装饰 过的函数,会将一个函数定义成一个属性,属性的值就是该函数return的内容。同时,会将这个函数变成另外一个装饰器。就像@age.setter和@age.deleter。
@age.setter 使得我们可以使用p1.age = 25这样的方式直接赋值;@age.deleter 使得我们可以使用del p1.age这样的方式来删除属性。