7.1 创建装饰器
与java中的装饰器模式类似,其作用就是将一些多余的、能够重复使用的代码抽离出来,然后通过python的语法糖@作用在方法头部,能够起到对方法增进、装饰的作用。类似的像统计功能,log日志功能等,都可以作为装饰器的模式供其他方法使用。
下面的实例是实现一个数列,类似1, 1, 2, 3, 5, 8,13... ...一个数等于前两个数之和,能够查询具体的第n个数。
def fibonacci(n):
if n <= 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(5))
实现逻辑很简单就是重复递归,最后一直调用到fibonacci(0), fibonacci(1)的值时才停止。但是仔细分析可知,在此过程中会造成一些不必要的计算,例如输出的n为5,下一步计算的是fibonacci(4) + fibonacci(3),再下一步(fibonacci(3) + fibonacci(2)) + (fibonacci(2) + fibonacci(1)),可以看出fibonacci(3)重复计算了。这种在数目特别大的情况下会导致运行速度很慢。
如果需要求出第50项的数,就需要去掉一些重复的计算,可以通过添加缓存机制来解决。
def fibonacci(n, cache=None):
if cache is None:
cache = {}
if n in cache:
return cache[n]
if n <= 1:
return 1
cache[n] = fibonacci(n - 1, cache) + fibonacci(n - 2, cache)
return cache[n]
print(fibonacci(50))
添加参数cache,当cache为空时,创建一个空的字典,将递归运行的结果存储在cache中,当之后递归遇到相同的结果时直接从缓存字典中取出即可,这样实现之后,大数字的运行速度会有明显提升。
类似的很多其他算法也可能会遇到此类问题,需要建立一个缓存机制进行处理,所以有必要将缓存机制封装出来供其他方法调用。下面是创建的memo方法,此方法接收的是函数闭包对象,然后创建内函数并在外层函数返回此内函数对象。在内函数中接收传来的参数,进行对应处理操作。
在调用memo方法时传入方法本身,并且生成一个新的方法对象,然后在这个新的方法对象中传入想要的参数值。
def memo(func):
cache = {}
def wrap(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrap
def fibonacci(n):
if n <= 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
fibonacci = memo(fibonacci)
print(fibonacci(50))
在python中对装饰器有个实用的语法糖,就是在使用装饰器的方法上添加@加上装饰器的方法名即可。
def memo(func):
cache = {}
def wrap(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrap
@memo
def fibonacci(n):
if n <= 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
# fibonacci = memo(fibonacci)
print(fibonacci(50))
7.2 为装饰器的函数保留元数据
在python中函数也属于对象,函数的元数据相当于对象的属性。下面看以下常用的函数属性。
def f(a, b=10):
''' f test method
:param a:
:param b:
:return:
'''
d = 10
print("f")
print(f.__doc__)
print(f.__defaults__)
print(f.__name__)
print(f.__module__)
f test method
:param a:
:param b:
:return:
(10,)
f
__main__
doc函数的文档
defaults函数的默认参数
name函数名
module函数所属的模块
另外,对于函数内部的闭包来说,可以通过函数的属性访问闭包。
def t():
a = 2
return lambda k: a ** k
g = t()
print(g.__closure__[0].cell_contents)
2
在t方法中有一个a字段,函数的返回值是一个lambda函数。当调用f方法时,a属性在闭包中,g变量可以通过closure访问函数闭包。
当使用装饰器时,装饰器函数可能会影响到调用的函数属性
def my_decortor(func):
def wrapper(*args, **kwargs):
"""wrapper func """
print("in wrapper")
func(*args, **kwargs)
return wrapper
@my_decortor
def example():
'''example func'''
print("example func")
print(example.__doc__)
print(example.__name__)
wrapper func
wrapper
因为在函数调用装饰器函数时,其函数对象已经发生了改变,变为了装饰器函数对象,所以调用example的函数元数据编成的是my_decortor的元数据了。
如果想保留原来调用函数的元数据,可以使用标准库functools中的wraps装饰内部包裹函数,可以将原来的函数属性更新到包裹函数中。
from functools import wraps, WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES, update_wrapper
def my_decortor(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""wrapper func """
print("in wrapper")
# update_wrapper(wrapper, func)
func(*args, **kwargs)
return wrapper
@my_decortor
def example():
'''example func'''
print("example func")
print(example.__doc__)
print(example.__name__)
example func
example
当给包裹函数加上装饰器@wraps,并传入原函数对象即可更新元数据到包裹函数上。其原理是调用update_wrapper(wrapper, func)方法,只不过使用装饰器调用更加简便。上面调用都是使用的默认参数,在装饰器@wraps的传参中可以传入其他的属性,默认参数具体为('module', 'name', 'qualname', 'doc', 'annotations')。
7.3 定义有参数的装饰器
带参数的装饰器,也就是根据参数定制化一个装饰器,可以看做生产装饰器的工厂。
例如创建一个装饰器,其具有判断函数的参数类型是否为所需类型。具体的思路是在普通的装饰器 外层再套上一个工厂方法来接收给装饰器传入的参数,然后通过方法签名信息inspect库中的signature来获取参数的信息,然后判断参数类型是否为装饰器定义类型,如果不是抛出异常即可。
from inspect import signature
def type_assert(*ty_args, **ty_kargs):
def decorator(func):
sig = signature(func)
btypes = sig.bind_partial(*ty_args, **ty_kargs).arguments
def wrapper(*args, **kwargs):
for name, obj in sig.bind(*args, **kwargs).arguments.items():
if name in btypes:
if not isinstance(obj, btypes[name]):
raise TypeError('"%s" must be "%s"' % (name, btypes[name]))
return func(*args, **kwargs)
return wrapper
return decorator
@type_assert(int, str, list)
def f(a, b, c):
print(a, b, c)
f(1, 1, 1)
signature主要是为了获取方法的相关信息,类似下面,能够获取出参数的名称、类型和默认值相关,并且通过bind方法绑定参数信息。
def test(a, b, c=1): pass
sig = signature(test)
print(sig.parameters)
print(sig.parameters['a'].name)
print(sig.parameters['a'].kind)
print(sig.parameters['c'].default)
bargs = sig.bind(str, int, int)
print(bargs.arguments['a'])
7.4 属性可修改的函数装饰器
在某些情况下,我们需要动态改函数装饰器的参数、属性值。
下面看一个实例,首先实现一个能够计算函数运行时长的装饰器,并且传入timeout参数,如果函数执行的时长超过了timeout规定,就打印出相关信息。
from functools import wraps
import time
import logging
from random import randint
def warn(timeout):
def decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
used = time.time() - start
if used > timeout:
msg = '"%s": %s > %s ' % (func.__name__, used, timeout)
logging.warning(msg)
return res
return wrapper
return decorator
@warn(1.5)
def test():
print('In test')
while randint(0, 1):
time.sleep(0.5)
for x in range(1, 31):
test()
在test方法中使用warn装饰器进行修饰,并且传入timeout为1.5的参数,之后随机地睡眠0.5秒,启动执行30次。可以看到随机打印出了log,WARNING:root:"test": 2.059203624725342 > 1.5
接下来,上限在外层动态修改timeout的值。
from functools import wraps
import time
import logging
from random import randint
def warn(timeout):
def decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
used = time.time() - start
if used > timeout:
msg = '"%s": %s > %s ' % (func.__name__, used, timeout)
logging.warning(msg)
return res
def set_timeout(k):
nonlocal timeout
timeout = k
wrapper.set_timeout = set_timeout
return wrapper
return decorator
@warn(1.5)
def test():
print('In test')
while randint(0, 1):
time.sleep(0.5)
# for x in range(1, 31):
# test()
test.set_timeout(1)
for x in range(1, 31):
test()
想要动态修改装饰器的参数,就需要对包裹函数增加属性,此属性可以是内函数,然后在内函数中对传入的装饰器参数进行进一步操作。需要注意的是,timeout是闭包内的数据,新建的内函数是不能直接访问到此参数,在python3中提供了nonlocal 修饰符可以访问到闭包内的数据。
执行结果,随机打印出WARNING:root:"test": 1.0000569820404053 > 1