來源:《流暢的Python》
@decorate def target(): print('running target()')
等價於:html
def target(): print('running target()') target = decorate(target)
最終結果是一樣的,可是target所指向的對象可能會發生變化。python
提示
@只不過是一個語法糖,它的做用正如上面的代碼所展現的一樣。那麼,帶參數的裝飾器是什麼個情況呢?
@out(args) # 首先執行out(args)函數,返回裏面的函數,接下來跟普通的裝飾器一樣。 def func(): pass
裝飾器的一大特性是,能把被裝飾的函數替換成其餘函數。第二個特性是,裝飾器在加載模塊時當即執行。緩存
在導入包時,進行初始化,而不用直接對其調用。閉包
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
爲什麼會出現這種情況呢?這是因爲:app
Python 不要求聲明變量,可是假定在函數定義體中
賦值的變量是局部變量。賦值包含:
=, +=
等。
閉包指延伸了做用域的函數,其中包含函數定義體中引用、可是不在定義體中定義的非全局變量。函數是否是匿名的沒有關係,關鍵是它能訪問定義體以外定義的非全局變量。模塊化
下面兩者是等價的:函數
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
>>> 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]
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 變量也受這個問題影響。spa
可是對數字、字符串、元組等不可變類型來講,只能讀取,不能更新。若是嘗試從新綁定,例如 count = count + 1
,其實會隱式建立局部變量 count。這樣,count 就不是自由變量了,所以不會保存在閉包中。3d
爲了解決這個問題,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
賦予新值,閉包中保存的綁定會更新。
若是不加該裝飾器,則不支持關鍵字參數,並且遮蓋了被裝飾函數的 __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
這是一項優化技術,它把耗時的函數的結果保存起來,避免傳入相同的參數時重複計算。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)
其中:
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」
建立一個裝飾器工廠函數,把參數傳給它,返回一個裝飾器,而後再把它應用到要裝飾的函數上。
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)