来源:《流畅的Python》
1. 装饰器基础知识
@decorate
def target():
print('running target()')
等价于:
def target():
print('running target()')
target = decorate(target)
最終結果是一樣的,但是target所指向的對象可能會發生變化。
提示
@只不過是一個語法糖,它的作用正如上面的代碼所展示的一樣。那麼,帶參數的裝飾器是什麼個情況呢?
@out(args) # 首先執行out(args)函數,返回裏面的函數,接下來跟普通的裝飾器一樣。
def func():
pass
装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行。
2. 何时执行装饰器
在導入包時,進行初始化,而不用直接對其調用。
3. 变量作用域规则
In [1]: def f1(a):
...: print(a)
...: print(b)
...:
In [2]: f1(3)
3
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-2-db0f80b394ed> in <module>()
----> 1 f1(3)
<ipython-input-1-c1318c6d0711> in f1(a)
1 def f1(a):
2 print(a)
----> 3 print(b)
4
NameError: name 'b' is not defined
In [5]: b = 6
In [6]: f1(3)
3
6
In [7]: b = 6
In [8]: def f2(a):
...: print(a)
...: print(b)
...: b = 9
...:
In [9]: f2(3)
3
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-9-ddde86392cb4> in <module>()
----> 1 f2(3)
<ipython-input-8-2304a03d7bfd> in f2(a)
1 def f2(a):
2 print(a)
----> 3 print(b)
4 b = 9
5
UnboundLocalError: local variable 'b' referenced before assignment
爲什麼會出現這種情況呢?這是因爲:
Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。赋值包含:=, +=
等。
4. 闭包
闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
下面兩者是等價的:
class Averager():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
- series 是 make_averager 函数的局部变量;
- 调用 avg(10) 时,make_averager 函数已经返回了,而它的本地作用域也一去不复返了。
- 在 averager 函数中,series 是自由变量(free variable)。
- 这也是一个优化的技巧。因为类的开销太大了,对于只有一个函数和一些变量的类,可以改成函数来实现。
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
>>> avg.__code__.co_freevars
('series',)
# avg.__closure__ 中的各个元素对应于 avg.__code__.co_freevars 中的一个名称。
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
5. nonlocal声明
def make_averager():
count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averager
這段代碼有問題,有看出來問題在哪兒碼?
Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>
当 count 是数字或任何不可变类型时,count += 1
语句的作用其实与 count= count + 1
一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。total 变量也受这个问题影响。
但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如 count = count + 1
,其实会隐式创建局部变量 count。这样,count 就不是自由变量了,因此不会保存在闭包中。
为了解决这个问题,Python 3 引入了 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
赋予新值,闭包中保存的绑定会更新。
6. functools.wraps 装饰器
如果不加該裝飾器,則不支持关键字参数,而且遮盖了被装饰函数的 __name__
和 __doc__
属性。
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
7. 使用functools.lru_cache做备忘
这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。LRU三个字母是“Least Recently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉。
from clockdeco import clock
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
print(fibonacci(6))
輸出:
[0.00000143s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00006199s] fibonacci(2) -> 1
...
[0.00026441s] fibonacci(6) -> 8
8
import functools
from clockdeco import clock
@functools.lru_cache() # <1>
@clock # <2>
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
print(fibonacci(6))
輸出:
[0.00000072s] fibonacci(0) -> 0
[0.00000143s] fibonacci(1) -> 1
[0.00006676s] fibonacci(2) -> 1
[0.00000215s] fibonacci(3) -> 2
[0.00009227s] fibonacci(4) -> 3
[0.00000143s] fibonacci(5) -> 5
[0.00011635s] fibonacci(6) -> 8
8
lru_cache 可以使用两个可选的参数来配置。它的签名是:
functools.lru_cache(maxsize=128, typed=False)
其中:
- maxsize:應爲2的賠數。
- typed:爲True時,f(1)於f(1.0)視爲不同的情況。
8. 单分派泛函数
from functools import singledispatch
from collections import abc
import numbers
import html
@singledispatch # <1>
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
@htmlize.register(str) # <2>
def _(text): # <3>
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)
@htmlize.register(numbers.Integral) # <4>
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)
@htmlize.register(tuple) # <5>
@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>'
@singledispatch
不是为了把 Java 的那种方法重载带入 Python。在一个类中 为同一个方法定义多个重载变体,比在一个函数中使用一长串 if/elif/elif/elif
块要更好。但是这两种方案都有缺陷,因为它们让代码单元(类或函数)承担的职责太多。@singledispath
的优点是支持模块化扩展: 各个模块可以为它支持的各个类型注册一个专门函数。
这个机制最好的文档是“PEP 443 — Single-dispatch genericfunctions”
9. 参数化装饰器
创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。
registry = set() # <1>
def register(active=True): # <2>
def decorate(func): # <3>
print('running register(active=%s)->decorate(%s)'
% (active, func))
if active: # <4>
registry.add(func)
else:
registry.discard(func) # <5>
return func # <6>
return decorate # <7>
@register(active=False) # <8>等於 @decorate
def f1():
print('running f1()')
@register() # <9>
def f2():
print('running f2()')
def f3():
print('running f3()')
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT): # <1>
def decorate(func): # <2>
def clocked(*_args): # <3>
t0 = time.time()
_result = func(*_args) # <4>
elapsed = time.time() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args) # <5>
result = repr(_result) # <6>
print(fmt.format(**locals())) # <7>
return _result # <8>
return clocked # <9>
return decorate # <10>
if __name__ == '__main__':
@clock() # <11>
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。