函数装饰器和闭包
装饰器基础知识
- 装饰器是可调用对象,其参数是另一个函数
- 装饰器会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象
def deco(func): def inner(): print("inner is called") return inner @deco def func(): print("func is called") # func被装饰器修改成inner func() print(func)
输出:
inner is called
<function deco.<locals>.inner at 0x00000155417B9F28>
Python何时执行装饰器
- 装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。
- 即在模块导入初始化时运行
registry = [] def register(func): print('running register(%s)' % func) registry.append(func) return func @register def func1(): print('func1 is running') @register def func2(): print('func2 is running') def func3(): print('func3 is running') def main(): # 在main()运行之前,register已经运行了两次 # 这两次就是在被装饰函数定义时 # 所以registry也已经被初始化 print('main is running') print('registry:',registry) func1() func2() func3() if __name__ == '__main__': main()
输出:
running register(<function func1 at 0x000001BAE35B9EA0>)
running register(<function func2 at 0x000001BAE35B9F28>)
main is running
registry: [<function func1 at 0x000001BAE35B9EA0>, <function func2 at 0x000001BAE35B9F28>]
func1 is running
func2 is running
func3 is running
变量作用域规则
- Python不要求声明变量,但是假定在函数定义体中有赋值的变量是局部变量
b = 3 def f1(a): print(a) # 这里b是局部变量,所以错误提示为在赋值之前使用 # 如果想获取全局变量b,需要加上global b print(b) b = 9 f1(1)
闭包
- 只有涉及嵌套函数时才有闭包问题
- 闭包指延伸了作用域的某个函数,它包含了不在函数内定义的非全局变量
def make_avg(): vars = [] def avg(value): vars.append(value) total = sum(vars) return total/len(vars) return avg # avg中包含了在make_avg中定义的vars列表 # 即使make_avg已经返回,但vars仍然被保留下来 avg = make_avg() print(avg(10)) print(avg(11)) print(avg(12)) # 闭包中引用的非自己定义的的非全局变量称为自由变量 # 自由变量的名称被保存在__code__.co_freevars中 # 自由变量的值被保存在__closure__[i].cell_contents中 print('avg.__code__.co_freevars:',avg.__code__.co_freevars) print('avg.__closure__[0].cell_contents',avg.__closure__[0].cell_contents)
输出:
10.0
10.5
11.0
avg.__code__.co_freevars: ('vars',)
avg.__closure__[0].cell_contents [10, 11, 12]
nonlocal声明
- 在闭包中对自由变量进行赋值,会生成一个局部变量覆盖自由变量,并且报错:局部变量使用前未赋值
def make_avg(): count=0 total=0 def avg(value): # count+=1就是count = count + 1 # 这里会自动生成一个未赋初始值的局部变量count覆盖自由变量count count+=1 # total和count相同情况 total+=value return total/count return avg avg = make_avg() print(avg(10)) print(avg(11)) print(avg(12)) 输出: UnboundLocalError: local variable 'count' referenced before assignment
- 使用nonlocal声明,不会覆盖自由变量
def make_avg(): count=0 total=0 def avg(value): # 使用nonlocal声明,不会覆盖自由变量 nonlocal count,total count+=1 total+=value return total/count return avg avg = make_avg() print(avg(10)) print(avg(11)) print(avg(12)) 输出: 10.0 10.5 11.0
一个函数执行时间的装饰器
- func的函数__name__和__doc__属性将被clocked相应属性覆盖
import time def clock(func): def clocked(*args): t0 = time.perf_counter() result = func(*args) elapsed = time.perf_counter() - t0 func_name = func.__name__ func_args = ','.join(str(args)) s = '[%0.8fs] %s(%s) --> %r' % (elapsed,func_name,func_args,result) print(s) return result return clocked @clock def snooze(n): time.sleep(n) @clock def factorial(n): if n == 1: return 1 else: return n*factorial(n-1) snooze(0.123) print('-' * 100) print('factorial(6):',factorial(6)) print('-' * 100) print('the name of factorial:',factorial.__name__) 输出: [0.12324016s] snooze((,0,.,1,2,3,,,)) --> None ---------------------------------------------------------------------------------------------------- [0.00000032s] factorial((,1,,,)) --> 1 [0.00001091s] factorial((,2,,,)) --> 2 [0.00002855s] factorial((,3,,,)) --> 6 [0.00003401s] factorial((,4,,,)) --> 24 [0.00003914s] factorial((,5,,,)) --> 120 [0.00004588s] factorial((,6,,,)) --> 720 factorial(6): 720 ---------------------------------------------------------------------------------------------------- the name of factorial: clocked
- 使用wraps装饰器后,func的__name__和__doc__将不会被覆盖
import time import functools def clock(func): # 使用wraps装饰器后,func的__name__和__doc__将不会被覆盖 @functools.wraps(func) def clocked(*args): t0 = time.perf_counter() result = func(*args) elapsed = time.perf_counter() - t0 func_name = func.__name__ func_args = ','.join(str(args)) s = '[%0.8fs] %s(%s) --> %r' % (elapsed,func_name,func_args,result) print(s) return result return clocked @clock def factorial(n): if n == 1: return 1 else: return n*factorial(n-1) print('the name of factorial:',factorial.__name__) 输出: the name of factorial: factorial
使用functools.lru_cache做备忘
import time import functools def clock(func): # 使用wraps装饰器后,func的__name__和__doc__将不会被覆盖 @functools.wraps(func) def clocked(*args): t0 = time.perf_counter() result = func(*args) elapsed = time.perf_counter() - t0 func_name = func.__name__ func_args = ','.join([str(a) for a in args if str(a) != '']) s = '[%0.8fs] %s(%s) --> %r' % (elapsed,func_name,func_args,result) print(s) return result return clocked # 不使用lru_cache,fib(6)要调用fib(1)8次,fic(2)5次等等 @clock def fib(n): if n < 2: return n else: return fib(n-2) + fib(n-1) # 使用lru_cache,可以缓存中间结果,避免重复调用。 @functools.lru_cache() @clock def fib2(n): if n < 2: return n else: return fib2(n-2) + fib2(n-1) print('fib(6):',fib(6)) print('-' * 100) print('fib2(6):',fib2(6)) 输出: [0.00000032s] fib(0) --> 0 [0.00000032s] fib(1) --> 1 [0.00004235s] fib(2) --> 1 [0.00000000s] fib(1) --> 1 [0.00000000s] fib(0) --> 0 [0.00000032s] fib(1) --> 1 [0.00001059s] fib(2) --> 1 [0.00002117s] fib(3) --> 2 [0.00007443s] fib(4) --> 3 [0.00000032s] fib(1) --> 1 [0.00000032s] fib(0) --> 0 [0.00000000s] fib(1) --> 1 [0.00001091s] fib(2) --> 1 [0.00002181s] fib(3) --> 2 [0.00000032s] fib(0) --> 0 [0.00000000s] fib(1) --> 1 [0.00001123s] fib(2) --> 1 [0.00000032s] fib(1) --> 1 [0.00000032s] fib(0) --> 0 [0.00000032s] fib(1) --> 1 [0.00001123s] fib(2) --> 1 [0.00002246s] fib(3) --> 2 [0.00004459s] fib(4) --> 3 [0.00007731s] fib(5) --> 5 [0.00016297s] fib(6) --> 8 fib(6): 8 ---------------------------------------------------------------------------------------------------- [0.00000000s] fib2(0) --> 0 [0.00000032s] fib2(1) --> 1 [0.00001476s] fib2(2) --> 1 [0.00000064s] fib2(3) --> 2 [0.00002695s] fib2(4) --> 3 [0.00000064s] fib2(5) --> 5 [0.00003882s] fib2(6) --> 8 fib2(6): 8
使用单分派函数
- 类似其他面向对象语言的方法重载,以不同方式执行相同操作的一组函数
from functools import singledispatch import html from collections import abc import numbers # 单分派函数的基函数 @singledispatch def htmlize(obj): return '<pre>{}<pre>'.format(html.escape(repr(obj))) # 各个专门函数使用@<base_function>.register(<type>)修饰 @htmlize.register(str) # 函数名无关紧要,_是个不错的选择,简单明了 def _(txt): s = html.escape(txt).replace('\n','<br>\n') return '<p>{}<p>'.format(s) # numbers.Integral是int的虚拟超类 @htmlize.register(numbers.Integral) def _(n): return '<pre>{0}(0x{0:X})</pre>'.format(n) # 可以叠放多个register装饰器,让同一个函数支持不同类型 @htmlize.register(tuple) @htmlize.register(abc.MutableSequence) def _(seq): inner = '</li>\n<li>'.join(htmlize(item) for item in seq) outer = '<ul>\n<li>' + inner + '</li>\n</ul>' return outer print(htmlize({1, 2, 3})) print(htmlize(abs)) print(htmlize(42)) print(htmlize(['alpha',66,{3,2,1}]))
输出:
<pre>{1, 2, 3}<pre>
<pre><built-in function abs><pre>
<pre>42(0x2A)</pre>
<ul>
<li><p>alpha<p></li>
<li><pre>66(0x42)</pre></li>
<li><pre>{1, 2, 3}<pre></li>
</ul>
一个参数化的装饰器
registry = set() # register是装饰器工厂,返回一个装饰器 # 它接受一个可选关键字参数 def register(active=True): # decorate是真正的装饰器,它的参数是一个函数 def decorate(func): print('running register(active=%s) -> decorate(%s)' % (active,func)) # active为真,注册func if active: registry.add(func) # active为假,删除func else: registry.discard(func) return func return decorate # register必须作为函数调用,并且可以传入参数 @register(active=True) def func1(): print('running func1') # 即使不传入参数,也要作为函数调用 @register() def func2(): print('running func2') # active为假,不注册func3 @register(active=False) def func3(): print('running func3') if __name__ == '__main__': func1() func2() func3() print(registry) 输出: running register(active=True) -> decorate(<function func1 at 0x00000201B1579F28>) running register(active=True) -> decorate(<function func2 at 0x00000201B1586048>) running register(active=False) -> decorate(<function func3 at 0x00000201B15860D0>) running func1 running func2 running func3 {<function func1 at 0x00000201B1579F28>, <function func2 at 0x00000201B1586048>}
三层嵌套的装饰器
import time default_fmt = '[{elapsed:0.8f}s] {name}({args}) -> {result}' # 一个三层嵌套的装饰器 def clock(fmt=default_fmt): # @clock()返回decorate def decorate(func): # 再把被装饰函数传递给decorate,运行它 # 返回clocked,替代被装饰函数 def clocked(*args): t0 = time.time() _result = func(*args) elapsed = time.time()-t0 name = func.__name__ args = ','.join(repr(item) for item in args) result = repr(_result) print(fmt.format(**locals())) return _result return clocked return decorate # func被clocked替代 @clock() def func(): time.sleep(0.123) if __name__ == '__main__': for i in range(3): func() 输出: [0.12314367s] func() -> None [0.12322927s] func() -> None [0.12370729s] func() -> None