《流暢的Python》筆記python
本篇將從最簡單的裝飾器開始,逐漸深刻到閉包的概念,而後實現參數化裝飾器,最後介紹標準庫中經常使用的裝飾器。程序員
函數裝飾器用於在源代碼中「標記」函數,以某種方式加強函數的行爲。裝飾器就是函數,或者說是可調用對象,它以另外一個函數爲參數,最後返回一個函數,但這個返回的函數並不必定是原函數。算法
如下是裝飾器最基本的用法:編程
# 代碼1
#裝飾器用法
@decorate
def target(): pass
# 上述代碼等價於如下代碼
def target(): pass
target = decorate(target)
複製代碼
即,最終的target
函數是由decorate(target)
返回的函數。下面這個例子說明了這一點:緩存
# 代碼2
def deco(func):
def inner():
print("running inner()")
return inner
@deco
def target():
print("running target()")
target()
print(target)
# 結果
running inner() # 輸出的是裝飾器內部定義的函數的調用結果
<function deco.<locals>.inner at 0x000001AF32547D90>
複製代碼
從上面可看出,裝飾器的一大特性是能把被裝飾的函數替換成其餘函數。但嚴格說來,裝飾器只是語法糖(語法糖:在編程語言中添加某種語法,但這種語法對語言的功能沒有影響,只是更方便程序員使用)。bash
裝飾器還能夠疊加。下面是一個說明,具體例子見後面章節:微信
# 代碼3
@d1
@d2
def f(): pass
#上述代碼等價於如下代碼:
def f(): pass
f = d1(d2(f))
複製代碼
裝飾器的另外一個關鍵特性是,它在被裝飾的函數定義後當即運行,這一般是在導入時,即Python加載模塊時:閉包
# 代碼4
registry = []
def register(func):
print("running register(%s)" % func)
registry.append(func)
return func
@register
def f1():
print("running f1()")
def f2():
print("running f2()")
if __name__ == "__main__":
print("running in main")
print("registry ->", registry)
f1()
f2()
# 結果
running register(<function f1 at 0x0000027745397840>)
running in main # 進入到主程序
registry -> [<function f1 at 0x0000027745397840>]
running f1()
running f2()
複製代碼
裝飾器register
在加載模塊時就對f1()
進行了註冊,因此當運行主程序時,列表registry
並不爲空。app
函數裝飾器在導入模塊時當即執行,而被裝飾的函數只在明確調用時運行。 這突出了Python程序員常說的導入時和運行時之間的區別。框架
裝飾器在真實代碼中的使用方式與代碼4
中有所不一樣:
代碼4
中的裝飾器原封不動地返回了傳入的函數。這種裝飾器並非沒有用,正如代碼4
中的裝飾器的名字同樣,這類裝飾器常充當了註冊器,不少Web框架就使用了這種方法。下一小節也是該類裝飾器的一個例子。
上一篇中咱們用Python函數改進了傳統的策略模式,其中,咱們定義了一個promos
列表來記錄有哪些具體策略,當時的作法是用globals()
函數來獲取具體的策略函數,如今咱們用裝飾器來改進這一作法:
# 代碼5,對以前的代碼進行了簡略
promos = []
def promotion(promo_func): # 只充當了註冊器
promos.append(promo_func)
return promo_func
@promotion
def fidelity(order): pass
@promotion
def bulk_item(order): pass
@promotion
def large_order(order): pass
def best_promo(order):
return max(promo(order) for promo in promos)
複製代碼
該方案相比以前的方案,有如下三個優勢:
_promo
結尾@promotion
裝飾器突出了被裝飾函數的做用,還便於臨時禁用某個促銷策略(只需將裝飾器註釋掉)正如前文所說,多數裝飾器會在內部定義函數,並將其返回,已替換掉傳入的函數。這個機制的實現就要靠閉包,但在理解閉包以前,先來看看Python中的變量做用域。
經過下述例子來解釋局部變量和全局變量:
# 代碼6
>>> def f1(a):
... print(a)
... print(b)
>>> f1(3)
3
Traceback (most recent call last):
-- snip --
NameError: name 'b' is not defined
複製代碼
當代碼運行到print(a)
時,Python查找變量a
,發現變量a
存在於局部做用域中,因而順利執行;當運行到print(b)
時,python查找變量b
,發現局部做用域中並無變量b
,便接着查找全局做用域,發現也沒有變量b
,最終報錯。正確的調用方式相信你們也知道,就是在調用f1(3)
以前給變量b
賦值。
咱們再看以下代碼:
# 代碼7
>>> b = 6
>>> def f2(a):
... print(a)
... print(b)
... b = 9
>>> f2(3)
3
Traceback (most recent call last):
-- snip --
UnboundLocalError: local variable 'b' referenced before assignment
複製代碼
按理說不該該報錯,而且b
的值應該打印爲6,但結果卻不是這樣。
事實是:變量b
原本是全局變量,但因爲在f2()
中咱們爲變量b
賦了值,因而Python在局部做用域中也註冊了一個名爲b
的變量(全局變量b
依然存在,有編程基礎的同窗應該知道,這叫作「覆蓋」)。當Python執行到print(b)
語句時,Python先搜索局部做用域,發現其中有變量b
,可是b
此時尚未被賦值(全局變量b
被覆蓋,而局部變量b
的賦值語句在該句後面),因而Python報錯。
若是不想代碼7
報錯,則須要使用global
語句,將變量b
聲明爲全局變量:
# 代碼8
>>> b = 6
>>> def f2(a):
... global b
... -- snip --
複製代碼
如今開始真正接觸閉包。閉包指延伸了做用域的函數,它包含函數定義體中引用,但不在定義體中定義的非全局變量,即這類函數能訪問定義體以外的非全局變量。只有涉及嵌套函數時纔有閉包問題。
下面用一個例子來講明閉包以及非全局變量。定義一個計算某商品一段時間內均價的函數avg
,它的表現以下:
# 代碼9
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
複製代碼
假定商品價格天天都在變化,所以須要一個變量來保存這些值。若是用類的思想,咱們能夠定義一個可調用對象,把這些值存到內部屬性中,而後實現__call__
方法,讓其表現得像函數;但若是按裝飾器的思想,能夠定義一個以下的嵌套函數:
# 代碼10
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
複製代碼
而後以以下方式使用這個函數:
# 代碼11
>>> avg = make_averager()
>>> avg(10)
10.0
-- snip --
複製代碼
不知道你們剛接觸這個內部的averager()
函數時有沒有疑惑:代碼11
中,當執行avg(10)
時,它是到哪裏去找的變量series
?series
是函數make_averager()
的局部變量,當make_averager()
返回了averager()
後,它的局部做用域就消失了,因此按理說series
也應該跟着消失,而且上述代碼應該報錯纔對。
事實上,在averager
函數中,series
是自由變量(free variable),即未在局部做用域中綁定的變量。這裏,自由變量series
和內部函數averager
共同組成了閉包,參考下圖:
實際上,Python在averager
的__code__
屬性中保存了局部變量和自由變量的名稱,在__closure__
屬性中保存了自由變量的值:
# 代碼12,注意這些變量的單詞含義,一目瞭然
>>> avg.__code__.co_varnames # co_varnames保存局部變量的名稱
('new_value', 'total')
>>> avg.__code__.co_freevars # co_freevars保存自由變量的名稱
('series',)
>>> avg.__closure__ # 單詞closure就是閉包的意思
# __closure__是一個cell對象列表,其中的元素和co_freevars元組一一對應
(<cell at 0x0000024EE023D7F8: list object at 0x0000024EDFE76288>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12] # cell對象的cell_contents屬性纔是真正保存自由變量的值的地方
複製代碼
綜上:閉包是一種函數,它會保存定義函數時存在的自由變量的綁定,這樣調用函數時,雖然外層函數的局部做用域不可用了,但仍能使用那些綁定。
注意:只有嵌套在其餘函數中的函數纔可能須要處理不在全局做用域中的外部變量。
代碼10
中的make_averager
函數並不高效,由於若是隻計算均值的話,其實不用保存每次的價格,咱們可按以下方式改寫代碼10
:
# 代碼13
def make_averager():
count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averager
複製代碼
但此時直接運行代碼11
的話,則會報代碼7
中的錯誤:UnboundLocalError
。
問題在於:因爲count
是不可變類型,在執行count += 1
時,該語句等價於count = count + 1
,而這就成了賦值語句,count
再也不是自由變量,而變成了averager
的局部變量。total
也是同樣的狀況。而在以前的代碼10
中沒有這個問題,由於series
是個可變類型,咱們只是調用series.append
,以及把它傳給了sum
和len
,它並無變爲局部變量。
**對於不可變類型來講,只能讀取,不能更新,不然會隱式建立局部變量。**爲了解決這個問題,Python3引入了nonlocal
聲明。它的做用是把變量顯式標記爲自由變量:
# 代碼14
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
-- snip --
複製代碼
瞭解了閉包後,如今開始正式使用嵌套函數來實現裝飾器。首先來認識標準庫中三個重要的裝飾器。
來看一個簡單的裝飾器:
# 代碼15
def deco(func):
def test():
func()
return test
@deco
def Test():
"""This is a test"""
print("This is a test")
print(Test.__name__)
print(Test.__doc__)
# 結果
test
None
複製代碼
咱們想讓裝飾器來自動幫咱們作一些額外的操做,但像改變函數屬性這樣的操做並不必定是咱們想要的:從上面能夠看出,Test
如今指向了內部函數test
,Test
自身的屬性被遮蓋。若是想保留函數本來的屬性,可使用標準庫中的functools.wraps
裝飾器。下面以一個更復雜的裝飾器爲例,它會在每次調用被裝飾函數時計時,並將通過的時間,傳入的參數和調用的結果打印出來:
# 代碼16
# clockdeco.py
import time, 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
複製代碼
它的使用將和下一個裝飾器一塊兒展現。
functools.lru_cache
實現了備忘(memoization)功能,這是一項優化技術,他把耗時的函數的結果保存起來,避免傳入相同參數時重複計算。以斐波那契函數爲例,咱們知道以遞歸形式實現的斐波那契函數會出現不少重複計算,此時,就可使用這個裝飾器。如下代碼是沒使用該裝飾器時的運行狀況:
# 代碼17
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.__name__)
print(fibonacci.__doc__)
print(fibonacci(6))
# 結果:
fibonacci # fibonacci本來的屬性獲得了保留
None
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(3) -> 2
[0.00000000s] fibonacci(4) -> 3
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(3) -> 2
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00049996s] fibonacci(2) -> 1
[0.00049996s] fibonacci(3) -> 2
[0.00049996s] fibonacci(4) -> 3
[0.00049996s] fibonacci(5) -> 5
[0.00049996s] fibonacci(6) -> 8
8
複製代碼
能夠看出,fibonacci(1)
調用了8次,下面咱們用functools.lru_cache
來改進上述代碼:
# 代碼18
import functools
from clockdeco import clock
@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))
# 結果:
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(3) -> 2
[0.00000000s] fibonacci(4) -> 3
[0.00000000s] fibonacci(5) -> 5
[0.00000000s] fibonacci(6) -> 8
8
複製代碼
functools.lru_cache
裝飾器能夠接受參數,而且此代碼還疊放了裝飾器。
lru_cache
有兩個參數:functools.lru_cache(maxsize=128, typed=False)
maxsize
指定存儲多少個調用的結果,該參數最好是2的冪。當緩存滿後,根據LRU算法替換緩存中的內容,這也是爲何這個函數叫lru_cache
。type
若是設置爲True
,它將把不一樣參數類型下獲得的結果分開保存,即把一般認爲相等的浮點數和整數參數分開(好比區分1和1.0)。lru_cache
使用字典存儲結果,字典的鍵是傳入的參數,因此被lru_cache
裝飾的函數的全部參數都必須是可散列的!咱們知道,C++支持函數重載,同名函數能夠根據參數類型的不一樣而調用相應的函數。以Python代碼爲例,咱們但願下面這個函數表現出以下行爲:
# 代碼19
def myprint(obj):
return "Hello~~~"
# 如下是咱們但願它擁有的行爲:
>>> myprint(1)
Hello~~~
>>> myprint([])
Hello~~~
>>> myprint("hello") # 即,當咱們傳入特定類型的參數時,函數返回特定的結果
This is a str
複製代碼
單憑這一個myprint
還沒法實現上述要求,由於Python不支持方法或函數的重載。爲了實現相似的功能,一種常見的作法是將函數變爲一個分派函數,使用一串if/elif/elif
來判斷參數類型,再調用專門的函數(如myprint_str
),但這種方式不利於代碼的擴展和維護,還顯得沒有B格。。。
爲解決這個問題,從Python3.4開始,可使用functools.singledispath
裝飾器,把總體方案拆分紅多個模塊,甚至能夠爲沒法修改的類提供專門的函數。被@singledispatch
裝飾的函數會變成泛函數(generic function),它會根據第一個參數的不一樣而調用響應的專門函數,具體用法以下:
# 代碼20
from functools import singledispatch
import numbers
@singledispatch
def myprint(obj):
return "Hello~~~"
# 能夠疊放多個register,讓同一函數支持不一樣類型
@myprint.register(str)
# 註冊的專門函數最好處理抽象基類,而不是具體實現,這樣代碼支持的兼容類型更普遍
@myprint.register(numbers.Integral)
def _(text): # 專門函數的名稱無所謂,使用 _ 能夠避免起名字的麻煩
return "Special types"
複製代碼
對泛函數的補充:根據參數類型的不一樣,以不一樣方式執行相同操做的一組函數。若是依據是第一個參數,則是單分派;若是依據是多個參數,則是多分派。
從上面諸多例子咱們能夠看到兩大類裝飾器:不帶參數的裝飾器(調用時最後沒有括號)和帶參數的裝飾器(帶括號)。Python將被裝飾的函數做爲第一個參數傳給了裝飾器函數,那裝飾器函數如何接受其餘參數呢?作法是:建立一個裝飾器工廠函數,在這個工廠函數內部再定義其它函數做爲真正的裝飾器。工廠函數代爲接受參數,這些參數做爲自由變量供裝飾器使用。而後工廠函數返回裝飾器,裝飾器再應用到被裝飾函數上。
咱們把1.2中代碼4
的@register
裝飾器改成帶參數的版本,以active
參數來指示裝飾器是否註冊某函數(雖然這麼作有點多餘)。這裏只給出@register
裝飾器的實現,其他代碼參考代碼4
:
# 代碼21
registry = set()
def register(active=True):
def decorate(func): # 變量active對於decorate函數來講是自由變量
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) # 即便不傳參數也要做爲函數調用@register()
def f():pass
# 上述用法至關於以下代碼:
# register(active=False)(f)
複製代碼
參數化裝飾器一般會把被裝飾函數替換掉,並且結構上須要多一層嵌套。下面以3.1.1中代碼16
裏的@clock
裝飾器爲例,讓它按用戶要求的格式輸出數據。爲了簡便,不調用functools.wraps
裝飾器:
# 代碼22
import time
DEFAULT_FMT = "[{elapsed:0.8f}s] {name}({args}) -> {result}"
def clock(fmt=DEFAULT_FMT): # 裝飾器工廠,fmt是裝飾器的參數
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())) #locals()函數以字典形式返回clocked的局部變量
return _result
return clocked
return decorate
複製代碼
能夠獲得以下結論:裝飾器函數有且只有一個參數,即被裝飾器的函數;若是裝飾器要接受其餘參數,請在本來的裝飾器外再套一層函數(工廠函數),由它來接受其他參數;而你最終使用的函數應該定義在裝飾器函數中,且它的參數列表應該和被裝飾的函數一致。
本篇首先介紹了最簡單裝飾器如何定義和使用,介紹了裝飾器在何時被執行,以及用最簡單的裝飾器改造了上一篇的策略模式;隨後更進一步,介紹了與閉包相關的概念,包括變量做用域,閉包和nonlocal聲明;最後介紹了更復雜的裝飾器,包括標準庫中的裝飾器的用法,以及如何定義帶參數的裝飾器。
但上述對裝飾器的描述都是基本的, 更復雜、工業級的裝飾器還須要更深刻的學習。
迎你們關注個人微信公衆號"代碼港" & 我的網站 www.vpointer.net ~