|
函數(shù)裝飾器可以被用于增強(qiáng)方法的某些行為,如果想自己實(shí)現(xiàn)裝飾器,則必須了解閉包的概念。 裝飾器的基本概念裝飾器是一個(gè)可調(diào)用對(duì)象,它的參數(shù)是另一個(gè)函數(shù),稱(chēng)為被裝飾函數(shù)。裝飾器可以修改這個(gè)函數(shù)再將其返回,也可以將其替換為另一個(gè)函數(shù)或者可調(diào)用對(duì)象。 例如:有個(gè)名為 decorate 的裝飾器: @decorate
def target():
print('running target()')上述代碼的寫(xiě)法和以下寫(xiě)法的效果是一樣的: def target():
print('running target()')
target = decorate(target)但是,它們返回的 target 不一定是原來(lái)的那個(gè) target 函數(shù),例如下面這個(gè)例子: >>> def deco(func):
... def inner():
... print('running inner()')
... return inner
...
>>> @deco
... def target():
... print('running target()')
...
>>> target()
running inner()
>>> target
<function deco.<locals>.inner at 0x0000013D88563040>可以看到,調(diào)用 target 函數(shù)執(zhí)行的是 inner 函數(shù),這里的 target 實(shí)際上是 inner 的引用。 何時(shí)執(zhí)行裝飾器裝飾器的另一個(gè)關(guān)鍵特性是,它們?cè)诒谎b飾函數(shù)定義時(shí)立即執(zhí)行,這通常是發(fā)生在導(dǎo)入模塊的時(shí)候。 例如下面的這個(gè)模塊:registration.py # 存儲(chǔ)被裝飾器 @register 裝飾的函數(shù)
registry = []
# 裝飾器
def register(func):
print(f"注冊(cè)函數(shù) -> {func}")
# 記錄被裝飾的函數(shù)
registry.append(func)
return func
@register
def f1():
print("執(zhí)行 f1()")
@register
def f2():
print("執(zhí)行 f2()")
def f3():
print("執(zhí)行 f3()")
if __name__ == "__main__":
print("執(zhí)行主函數(shù)")
print("registry -> ", registry)
f1()
f2()
f3()現(xiàn)在我們?cè)诿钚袌?zhí)行這個(gè)腳本: $ python registration.py
注冊(cè)函數(shù) -> <function f1 at 0x000001F6FC8320D0>
注冊(cè)函數(shù) -> <function f2 at 0x000001F6FC832160>
執(zhí)行主函數(shù)
registry -> [<function f1 at 0x000001F6FC8320D0>, <function f2 at 0x000001F6FC832160>]
執(zhí)行 f1()
執(zhí)行 f2()
執(zhí)行 f3() 這里我們可以看到,在主函數(shù)執(zhí)行之前,register 已經(jīng)執(zhí)行了兩次。加載模塊后,registry 中已經(jīng)有兩個(gè)被裝飾函數(shù)的引用:f1 和 f2。不過(guò)這兩個(gè)函數(shù)以及 f3 都是在腳本中明確調(diào)用后才開(kāi)始執(zhí)行的。 如果只是單純的導(dǎo)入 registration.py 模塊而不運(yùn)行: >>> import registration
注冊(cè)函數(shù) -> <function f1 at 0x0000022670012280>
注冊(cè)函數(shù) -> <function f2 at 0x0000022670012310> 查看 registry 中的值: >>> registration.registry
[<function f1 at 0x0000022670012280>, <function f2 at 0x0000022670012310>] 這個(gè)例子主要說(shuō)明:裝飾器在導(dǎo)入模塊時(shí)立即執(zhí)行,而被裝飾的函數(shù)只有在明確調(diào)用時(shí)才運(yùn)行。這也突出了 Python 中導(dǎo)入時(shí)和運(yùn)行時(shí)這個(gè)兩個(gè)概念的區(qū)別。 在裝飾器的實(shí)際使用中,有兩點(diǎn)和示例是不同的: 裝飾器內(nèi)部定義并返回新函數(shù)的做法需要靠閉包才能正常運(yùn)作。為了理解閉包,則必須先了解 Python 中的變量作用域。 變量作用域的規(guī)則我們來(lái)看下面這個(gè)例子,一個(gè)函數(shù)讀取一個(gè)局部變量 a,一個(gè)全局變量 b。 >>> def f1(a):
... print(a)
... print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
NameError: name 'b' is not defined 出現(xiàn)錯(cuò)誤并不奇怪。如果我們先給 b 賦值,再調(diào)用 f1,那就不會(huì)出錯(cuò)了: >>> b = 1
>>> f1(3)
3
1 現(xiàn)在,我們來(lái)看一個(gè)不尋常的例子: >>> b = 1
>>> def f2(a):
... print(a)
... print(b)
... b = 2
...
>>> f2(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment 這里,f2 函數(shù)的前兩行和 f1 相同,然后再給 b 賦值??墒?,在賦值之前,第二個(gè) print 失敗了。這是因?yàn)?strong>Python 在編譯函數(shù)的定義體時(shí),發(fā)現(xiàn)在函數(shù)中有給 b 賦值的語(yǔ)句,因此判斷它是局部變量。而在上述示例中,當(dāng)我們打印局部變量 b 時(shí),它并沒(méi)有被綁定值,故而報(bào)錯(cuò)。 Python 不要求聲明變量,但是會(huì)把在函數(shù)定義體中賦值的變量當(dāng)成局部變量。
如果想把上述示例中的 b 看成全局變量,則需要使用 global 聲明: >>> b = 1
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 2
...
>>> f3(3)
3
1
>>> b
2
>>> f3(3)
3
2 閉包閉包是指延伸了作用域的函數(shù),其中包含了函數(shù)定義體中的引用,以及不在定義體中定義的非全局變量。 我們通過(guò)以下示例來(lái)理解這句話(huà)。 假設(shè)我們有這種需求,計(jì)算某個(gè)商品在整個(gè)歷史中的平均收盤(pán)價(jià)格(商品每天的價(jià)格會(huì)變化)。例如: >>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0 那么如何獲取 avg 函數(shù)?歷史收盤(pán)價(jià)格又是如何保存的? 我們可以用一個(gè)類(lèi)來(lái)實(shí)現(xiàn): class Averager:
def __init__(self):
self.serial = []
def __call__(self, price):
self.serial.append(price)
return sum(self.serial) / len(self.serial) Averager 的實(shí)例是一個(gè)可調(diào)用對(duì)象。
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0 也可以使用一個(gè)函數(shù)來(lái)實(shí)現(xiàn): >>> def make_averager():
... serial = []
... def averager(price):
... serial.append(price)
... return sum(serial) / len(serial)
... return averager
...
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0 第一種寫(xiě)法很明顯的可以看到,所有歷史收盤(pán)價(jià)均保存在實(shí)例變量 self.serial 中。 第二種寫(xiě)法我們要好好的分析一下:serial 是 make_averager 的局部變量,但是當(dāng)我們調(diào)用 avg(10) 時(shí),make_averager 函數(shù)已經(jīng)返回了,它的作用域不是應(yīng)該消失了嗎? 實(shí)際上,在 averager 函數(shù)中,serial 是自由變量(未在本地作用域中綁定的變量)。如下圖所示: 
我們可以在 averager 返回對(duì)象的 __code__ 屬性中查看它的局部變量和自由變量的名字。 >>> avg.__code__.co_varnames
('price',)
>>> avg.__code__.co_freevars
('serial',)自由變量 serial 綁定的值存放在 avg 對(duì)象的 __closure__ 屬性中,它是一個(gè)元組,里面的元素是 cell 對(duì)象,它的 cell_contents 屬性保存實(shí)際的值: >>> avg.__closure__
(<cell at 0x000002266FF99430: list object at 0x00000226702841C0>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12] 綜上所述,閉包是一種函數(shù),它會(huì)保留定義函數(shù)時(shí)存在的自由變量的綁定值,這樣在我們調(diào)用這個(gè)函數(shù)時(shí),即使作用域不在了,仍然可以使用這些綁定的值。 注意: 只有嵌套在其它函數(shù)中的函數(shù)才可能需要處理不在全局作用域中的外部變量。
nonlocal 聲明
前面的 make_averager 方法的效率并不高,我們可以只保存當(dāng)前的總值和元素個(gè)數(shù),再使用它們計(jì)算平均值。下面是我們更改后的函數(shù)體: >>> def make_averager():
... count = total = 0
... def averager(price):
... count += 1
... total += price
... return total / count
... return averager 但是這個(gè)寫(xiě)法實(shí)際上是有問(wèn)題的,我們先運(yùn)行再分析: >>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in averager
UnboundLocalError: local variable 'count' referenced before assignment 這里 count 被當(dāng)成 averager 的局部變量,而不是我們期望的自由變量。這是因?yàn)?count += 1 相當(dāng)于 count = count + 1。因此,我們?cè)?averager 函數(shù)體中實(shí)際包含了給 count 賦值的操作,這就把 count 變成局部變量。total 也有這個(gè)問(wèn)題。 為了解決這個(gè)問(wèn)題,Python3 引入了 nonlocal 關(guān)鍵字,用于聲明自由變量。使用 nonlocal 修改上述的例子: >>> def make_averager():
... count = total = 0
... def averager(price):
... nonlocal count, total
... count += 1
... total += price
... return total / count
... return averager
...
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0 疊放裝飾器如果我們把 @d1 和 @d2 兩個(gè)裝飾器應(yīng)用到同一個(gè)函數(shù) f() 上,實(shí)際相當(dāng)于 f = d1(d2(f))。 也就是說(shuō),下屬代碼: @d1
@d2
def f():
pass 等同于: def f():
pass
f = d1(d2(f)) 參數(shù)化裝飾器Python 會(huì)把被裝飾的參數(shù)作為第一個(gè)參數(shù)傳遞給裝飾器函數(shù),那么如何讓裝飾器接受其它的參數(shù)呢?這里我們需要定義一個(gè)裝飾器工廠函數(shù),返回真正的裝飾器函數(shù)。 以本文開(kāi)頭的 register 裝飾器為例,我們?yōu)樗砑右粋€(gè) active 參數(shù),如果置為 False,那就不注冊(cè)這個(gè)函數(shù)。 registry = []
def register(active=True):
def decorate(func):
if active:
print(f"注冊(cè)函數(shù) -> {func}")
# 記錄被裝飾的函數(shù)
registry.append(func)
return func
return decorate
@register()
def f1():
print("執(zhí)行 f1")
@register(active=False)
def f2():
print("執(zhí)行 f2")現(xiàn)在我們導(dǎo)入這個(gè)模塊: >>> import registration
注冊(cè)函數(shù) -> <function f1 at 0x0000016D80402280> 可以看到只注冊(cè)了 f1 函數(shù)。 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的裝飾器這里我們使用嵌套函數(shù)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的裝飾器:計(jì)算被裝飾函數(shù)執(zhí)行的耗時(shí),并將函數(shù)名、參數(shù)和執(zhí)行的結(jié)果打印出來(lái)。 import time
def clock(func):
def clocked(*args):
start_time = time.perf_counter()
result = func(*args)
cost = time.perf_counter() - start_time
print(
"[%.2f] %s(%s) -> %r" % (cost, func.__name__, list(map(repr, args)), result)
)
return result
return clocked 下面我們來(lái)試試這個(gè)裝飾器: >>> @clock
... def factorial(n):
... # 計(jì)算 n 的階乘
... return 1 if n < 2 else n * factorial(n - 1)
>>>
>>> factorial(6)
[0.00] factorial(['1']) -> 1
[0.00] factorial(['2']) -> 2
[0.00] factorial(['3']) -> 6
[0.00] factorial(['4']) -> 24
[0.00] factorial(['5']) -> 120
[0.00] factorial(['6']) -> 720
720 具體來(lái)分析一下,這里 factorial 作為 func 參數(shù)傳遞給 clock 函數(shù),然后 clock 函數(shù)返回 clocked 函數(shù),Python 解釋器會(huì)把 clocked 賦值給 factorial。所以,如果我們查看 factorial 的 __name__ 屬性,會(huì)發(fā)現(xiàn)它的值是 clocked 而不是 factorial。 >>> factorial.__name__
'clocked' 所以,factorial 保存的是 clocked 的引用,每次調(diào)用 factorial 實(shí)際上都是在調(diào)用 clocked 函數(shù)。 我們也可以使用 functools.wraps 裝飾器把 func 的一些屬性復(fù)制到 clocked 函數(shù)上,例如:__name__ 和 __doc__: def clock(func):
@functools.wraps(func)
def clocked(*args):
start_time = time.perf_counter()
result = func(*args)
cost = time.perf_counter() - start_time
print(
"[%.2f] %s(%s) -> %r" % (cost, func.__name__, list(map(repr, args)), result)
)
return result
return clocked
>>>
>>> @clock
... def factorial(n):
... return 1 if n < 2 else n * factorial(n - 1)
>>>
>>> factorial.__name__
'factorial' 標(biāo)準(zhǔn)庫(kù)中的裝飾器使用 functools.lru_cache 做備忘functools.lru_cache 會(huì)把耗時(shí)的函數(shù)的結(jié)果保存起來(lái),避免傳入相同的參數(shù)時(shí)的重復(fù)計(jì)算。lru 的意思是 Least Recently Used,表示緩存不會(huì)無(wú)限增長(zhǎng),一段時(shí)間不用的緩存條目會(huì)被丟棄。
lru_cache 非常適合計(jì)算第 n 個(gè)斐波那契數(shù)這樣的慢速遞歸函數(shù)。
我們來(lái)看看不使用 lru_cache 時(shí)的情況: >>> @clock
... def fibonacci(n):
... return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)
...
>>> fibonacci(6)
[0.00000040] fibonacci(['0']) -> 0
[0.00000060] fibonacci(['1']) -> 1
[0.00030500] fibonacci(['2']) -> 1
[0.00000030] fibonacci(['1']) -> 1
[0.00000040] fibonacci(['0']) -> 0
[0.00000060] fibonacci(['1']) -> 1
[0.00042110] fibonacci(['2']) -> 1
[0.00074440] fibonacci(['3']) -> 2
[0.00128530] fibonacci(['4']) -> 3
[0.00000020] fibonacci(['1']) -> 1
[0.00000030] fibonacci(['0']) -> 0
[0.00000050] fibonacci(['1']) -> 1
[0.00035500] fibonacci(['2']) -> 1
[0.00055270] fibonacci(['3']) -> 2
[0.00000030] fibonacci(['0']) -> 0
[0.00000060] fibonacci(['1']) -> 1
[0.00041220] fibonacci(['2']) -> 1
[0.00000040] fibonacci(['1']) -> 1
[0.00000040] fibonacci(['0']) -> 0
[0.00000050] fibonacci(['1']) -> 1
[0.00032410] fibonacci(['2']) -> 1
[0.00061420] fibonacci(['3']) -> 2
[0.00122760] fibonacci(['4']) -> 3
[0.00206850] fibonacci(['5']) -> 5
[0.00352630] fibonacci(['6']) -> 8
8 這種方式有很多重復(fù)的計(jì)算,例如 fibonacci(['1']) 執(zhí)行了 8 次,fibonacci(['2']) 執(zhí)行了 5 次等等。 現(xiàn)在我們使用 functools.lru_cache 優(yōu)化一下: >>> @functools.lru_cache
... @clock
... def fibonacci(n):
... return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)
...
>>> fibonacci(6)
[0.00000060] fibonacci(['0']) -> 0
[0.00000070] fibonacci(['1']) -> 1
[0.00106320] fibonacci(['2']) -> 1
[0.00000080] fibonacci(['3']) -> 2
[0.00132790] fibonacci(['4']) -> 3
[0.00000060] fibonacci(['5']) -> 5
[0.00159670] fibonacci(['6']) -> 8
8 可以看到節(jié)省了一半的執(zhí)行時(shí)間,并且 n 的每個(gè)值只調(diào)用了一次函數(shù)。 在執(zhí)行 fibonacci(30) 時(shí),如果使用未優(yōu)化的版本需要 141 秒,使用優(yōu)化后的版本只需要 0.002 秒。 除了優(yōu)化遞歸算法之外,lru_cache 在從 WEB 獲取信息的應(yīng)用中也能發(fā)揮巨大作用。 lru_cache 還有兩個(gè)可選參數(shù):
def lru_cache(maxsize=128, typed=False): maxsize:最多可存儲(chǔ)的調(diào)用結(jié)果的個(gè)數(shù)。緩存滿(mǎn)了之后,舊的結(jié)果被丟棄。為了獲取最佳的性能,maxsize 應(yīng)該設(shè)置為 2 的冪。
typed:如果置為 True,會(huì)把不同參數(shù)類(lèi)型得到的結(jié)果分開(kāi)保存。例如:f(3.0) 和 f(3) 會(huì)被當(dāng)成不同的調(diào)用。
單分派泛函數(shù)假設(shè)我們現(xiàn)在開(kāi)發(fā)一個(gè)調(diào)試 WEB 應(yīng)用的工具:生成 HTML,顯示不同類(lèi)型的 Python 對(duì)象。 我們可以這樣編寫(xiě)一個(gè)函數(shù): import html
def htmlize(obj):
content = html.escape(repr(obj))
return f"<pre>{content}</pre>"現(xiàn)在我們需要做一些拓展,讓它使用特別的方式顯示某些特定類(lèi)型: str:把字符串內(nèi)部的 \n 替換為 <br>\n,并且使用 <p> 替換 <pre>;
int:以十進(jìn)制和十六進(jìn)制顯示數(shù)字;
list:顯示一個(gè) HTML 列表,根據(jù)各個(gè)元素的類(lèi)型格式化;
最常用的方式就是寫(xiě) if...elif..else 判斷: import numbers
from collections.abc import MutableSequence
def htmlize(obj):
if isinstance(obj, str):
content = obj.replace("\n", "<br>\n")
return f"<p>{content}</p>"
elif isinstance(obj, numbers.Integral):
content = f"{obj} ({hex(obj)})"
return f"<pre>{content}</pre>"
elif isinstance(obj, MutableSequence):
content = "</li>\n<li>".join(htmlize(item) for item in obj)
return "<ul>\n<li>" + content + "</li>\n</ul>"
else:
content = f"<pre>{obj}</pre>"
return content如果想添加新的類(lèi)型判斷,只會(huì)將函數(shù)越寫(xiě)越長(zhǎng),并且各個(gè)類(lèi)型之間耦合度較高,不利于維護(hù)。 Python 3.4 新增的 functools.singledispatch 裝飾器可以將整個(gè)方案拆分成多個(gè)模塊。 import numbers
from collections.abc import MutableSequence
from functools import singledispatch
@singledispatch
def htmlize(obj):
content = f"<pre>{obj}</pre>"
return content
@htmlize.register(str)
def _(text):
content = text.replace("\n", "<br>\n")
return f"<p>{content}</p>"
@htmlize.register(numbers.Integral)
def _(num):
content = f"{num} ({hex(num)})"
return f"<pre>{content}</pre>"
@htmlize.register(MutableSequence)
def _(seq):
content = "</li>\n<li>".join(htmlize(item) for item in seq)
return "<ul>\n<li>" + content + "</li>\n</ul>"這里我們?yōu)槊恳粋€(gè)需要特殊處理的類(lèi)型都定義另一個(gè)專(zhuān)門(mén)的函數(shù)。 functools.singledispatch 的更詳細(xì)的文檔參考:https://www./dev/peps/pep-0443/。
|