2021.6.24
6.nonlocal声明 计算移动平均值的高阶函数,不保存所有历史值,但有缺陷
def make_averager():
count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total/count
return averager
avg = make_averager()
print(avg(10))
会出现: 问题是,当count是数字或任何不可变类型时吗,count += 1语句的作用其实与count = count + 1一样。因此,我们在averager的定义体重为count赋值了,这会把count变成局部变量。total变量也受这个问题影响。
为了解决这个问题,Python3中引入了nonlocal声明。它的作用是把变量标记为自由变量。即使在函数中为变量赋予新值了,也会变成自由变量。如果为nonlocal声明的变量赋予新值,闭包中保存的绑定会更新。
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total/count
return averager
avg = make_averager()
print(avg(10))
7.实现一个简单的装饰器 定义一个装饰器,它会在每次调用被装饰的函数时计时,然后把经过的时间、传入的参数和调用的结果打印出来。
import time
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
if __name__=='__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))
但是以上的程序有几个缺点:不支持关键字参数,而且遮盖了被装饰函数的__name__和__doc__属性。下面使用functool.wraps装饰器把相关的属性从func复制到clocked中。
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
arg_lst.append(', '.join(pairs))
arg_str = ', '.join(arg_lst)
print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
return result
return clocked
8.标准库中的装饰器 使用functools.lru_cache做备忘录,它实现了备忘功能。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。 Least Recently Used 生成n个斐波纳契数,递归方式非常耗时。
import time
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__=='__main__':
print(fibonacci(6))
浪费时间的地方很明显,fibonacci(1)调用了8次,fibonacci(2)调用了5次,但是如果增加两行代码,使用lru_cache,性能会显著改善。
import time
import functools
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
@functools.lru_cache()
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__=='__main__':
print(fibonacci(6))
单分派泛函数 假设我们在开发一个调试Web应用的工具,我们想生成HTML,显示不同类型的Python对象 我们这样编写:
import html
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
print(htmlize({1, 2, 3}))
print(htmlize(abs))
print(htmlize('Heimlich & Co.\n- a game'))
print(htmlize(42))
print(htmlize(['alpha', 66, {3, 2, 1}]))
因为Python不支持重载方法或函数,所以我们不能使用不同的签名定义htmlize的变体,也无法使用不同的方式处理不同的数据类型。在Python中,一种常见的做法是吧htmlize变成一个分派函数,使用一串if/elif/elif,调用专门的函数,如htmlize_str、htmlize_int,等等。这样不便于模块的用户扩展,还显得笨拙,时间一长,分派函数会变得很大,而且它与各个专门函数之间的耦合也很紧密。
使用functools.singledispatch装饰器可以把整体方案拆分成多个模块,甚至可以为你修改的类提供专门的函数。
from functools import singledispatch
from collections import abc
import numbers
import html
@singledispatch
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
@htmlize.register(str)
def _(text):
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)
@htmlize.register(numbers.Integral)
def _(n):
return '<pre>{0} (0x{0:x}</pre>'.format(n)
@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n<ul>'
9.叠放装饰函数 把@d1和@d2两个装饰器按顺序应用到f函数上,作用相当于f = d1(d2(f)) 也就是说,下述代码具有等同:
@d1
@d2
def f():
print('f')
def f():
print('f')
f = d1(d2(f))
10.参数化装饰器 解析源码中的装饰器时,Python把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
print('running main()')
print('registry ->', registry)
f1()
为了便于启用或禁用register执行的函数注册功能,我们为它提供一个可选的active参数,设为False时,不注册被装饰的函数。
registry = set()
def register(active=True):
def decorate(func):
print('running register(active=%s)->decorate(%s)' % (active, func))
if active:
registry.add(func)
else:
registry.discard(func)
return func
return decorate
@register(active=False)
def f1():
print('running f1()')
@register()
def f2():
print('running f2()')
def f3():
print('running f3()')
参数化clock装饰器
import time
DEFAULT_FTM = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FTM):
def decorate(func):
def clocked(*_args):
t0 = time.time()
_result = func(*_args)
elapsed = time.time() -t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args)
result = repr(_result)
print(fmt.format(**locals()))
return _result
return clocked
return decorate
if __name__ == '__main__':
@clock()
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
运行结果: 上面的新功能:
@clock('{name}: {elapsed}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
运行结果:
|