《流畅的Python》 读书笔记 第7章_函数装饰器和闭包
第7章 函數裝飾器和閉包
裝飾器這個名稱可能更適合在編譯器領域使用,因為它會遍歷并注解句法樹
函數裝飾器用于在源碼中“標記”函數,以某種方式增強函數的行為。這是一項強大的功能,但是若想掌握,必須理解
閉包
如果你想自己實現函數裝飾器,那就必須了解閉包的方方面面,因此也就需要知道 nonlocal
閉包還是回調式異步編程和函數式編程風格的基礎
本章的最終目標是解釋清楚函數裝飾器的工作原理,包括最簡單的注冊裝飾器和較復雜的參數化裝飾器
討論如下話題
- Python 如何計算裝飾器句法
- Python 如何判斷變量是不是局部的
- 閉包存在的原因和工作原理
- nonlocal 能解決什么問題
再進一步探討
- 實現行為良好的裝飾器
- 標準庫中有用的裝飾器
- 實現一個參數化裝飾器
7.1 裝飾器基礎知識
裝飾器是可調用的對象,其參數是另一個函數(被裝飾的函數)
裝飾器可能會處理被裝飾的函數,然后把它返回,或者將其替換成另一個函數或可調用對象
@decorate
def target():
print('running target()')
等價于
def target():
print('running target()')
target = decorate(target)
此處的decorate是你定義好的裝飾器,姑且認為是個函數
這個函數被更改了,這也是網上流傳裝飾器萬能公式,記住了這點其實理解裝飾器或寫個簡單的裝飾器是很容易的。
裝飾器的適用范圍非常廣泛,你可以參考《7.12 延伸閱讀- 關于裝飾器的一個典型應用》
來看一個完整的例子
def deco(func):
def inner():
func() # 注: 此處我加的
print('running inner()')
return inner #?
@deco
def target(): #?
print('running target()')
target() #?
print(target) #?
? deco 返回 inner 函數對象。
? 使用 deco 裝飾 target。
? 調用被裝飾的 target 其實會運行 inner。
? 審查對象,發現 target 現在是 inner 的引用
執行結果
running target()
running inner()
<function deco.<locals>.inner at 0x00000190D7E77D30>
可以看到如果target沒加這個裝飾器,肯定是單單執行running target(),但加了裝飾器后
看似target執行可以多出來running inner(),實際上此時的target已經不再是原來的它了,它變了
根據萬能公式
@deco
def target():
pass
你這樣后會讓target變為target = deco(target)
再根據deco的定義
def deco(func):
...
return inner #?
你在執行deco(target)的時候,返回的是一個叫inner的東西
因為你最終執行的是target(),所以也就是inner()
再看inner定義
def inner():
func()
print('running inner()')
inner()的時候,會執行func(),func來自deco的實參,此處對應target,所以你會先執行target(),再執行print('running inner()')
裝飾器只是語法糖
裝飾器可以像常規的可調用對象那樣調用,其參數是另一個函數。有時,這樣做更方便,尤其是做元編程(在運行時改變程序的行為)時
裝飾器的一大特性是,能把被裝飾的函數替換成其他函數
第二個特性是,裝飾器在加載模塊時立即執行
7.2 Python何時執行裝飾器
裝飾器的一個關鍵特性是,它們在被裝飾的函數定義之后立即運行。這通常是在導入時(即 Python 加載模塊時)
書中的示例 registration.py 模塊
registry = [] #?
def register(func): #?
print('running register(%s)' % func) #?
registry.append(func) #?
return func #?
@register #?
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3(): #?
print('running f3()')
def main(): #?
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__=='__main__':
main() # ?
? registry 保存被 @register 裝飾的函數引用。
? register 的參數是一個函數。
? 為了演示,顯示被裝飾的函數。
? 把 func 存入 registry。
? 返回 func:必須返回函數;這里返回的函數與通過參數傳入的一樣。
? f1 和 f2 被 @register 裝飾。
? f3 沒有裝飾。
? main 顯示 registry,然后調用 f1()、f2() 和 f3()。
? 只有把 registration.py 當作腳本運行時才調用 main()。
我做了一些測試
- 21行的main()不寫,直接就一個pass,也會執行
running register(<function f1 at 0x000001940F3D7EE0>)
running register(<function f2 at 0x000001940F3F6040>)
- 這個跟你import 這個py文件的效果是一樣的,也充分說明了
在導入時立即運行 - 這也是為何你在打印registry這個列表的時候已經能看到里面有2個
- 類似的你把21行改為f1(),會打印如下。注意,有了上面的概念,你可能反而會覺得是不是會多打印一個
running register...,實則不然。
running register(<function f1 at 0x0000021998027E50>)
running register(<function f2 at 0x0000021998027D30>)
running f1()
- 最終寫上main()的運行效果
running register(<function f1 at 0x000002A0F6CF7E50>)
running register(<function f2 at 0x000002A0F6CF7D30>)
running main()
registry -> [<function f1 at 0x000002A0F6CF7E50>, <function f2 at 0x000002A0F6CF7D30>]
running f1()
running f2()
running f3()
函數裝飾器在導入模塊時立即執行,而被裝飾的函數只在明確調用時運行。這突出了 Python 程序員所說的導入時和運行時之間的區別
- 裝飾器函數與被裝飾的函數在同一個模塊中定義。實際情況是,裝飾器通常在一個模塊中定義,然后應用到其他模塊中的函數上。
- register 裝飾器返回的函數與通過參數傳入的相同。實際上,大多數裝飾器會在內部定義一個函數,然后將其返回。
裝飾器原封不動地返回被裝飾的函數,但是這種技術并非沒有用處。很多 Python Web 框架使用這樣的裝飾器把函數添加到某種*注冊處,例如把URL 模式映射到生成 HTTP 響應的函數上的注冊處。這種注冊裝飾器可能會也可能不會修改被裝飾的函數
7.3 使用裝飾器改進“策略”模式
策略模式是第6章的內容,比較的拗口,就先不寫了。
TODO
7.4 變量作用域規則
>>> def f(a): print(a)
... print(b)
File "<stdin>", line 2
print(b)
IndentationError: unexpected indent
>>> def f(a):
... print(a)
... print(b)
...
>>> f(1)
1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f
NameError: name 'b' is not defined
在書中,最后一行是這樣的
NameError: global name 'b' is not defined
雖然顯示不同(從Python3.5開始的),但的確b還是那個global,用生成的字節碼可以說明這點
>>> from dis import dis
>>> dis(f)
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 CALL_FUNCTION 1
6 POP_TOP
3 8 LOAD_GLOBAL 0 (print)
10 LOAD_GLOBAL 1 (b) # 看這里
12 CALL_FUNCTION 1
14 POP_TOP
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
加一個b的定義就能正常輸出了
>>> b=2
>>> f(1)
1
2
再看另外一個例子
>>> b = 1
>>> def func(a):
... print(a)
... print(b)
... b = 1
...
>>> func(2) 你可以思考下會輸出什么?為什么?
2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in func
UnboundLocalError: local variable 'b' referenced before assignment
你可能會覺得應該打印b的值6,因為外面定義了一個全局變量b,而在print(b)之后的b=9是后面執行的, 不會打印9才是。
事實是,Python 編譯函數的定義體時,它判斷 b 是局部變量,因為在函數中給它賦值了
也就是說在函數中加了一句b = 1,下面的就是b就從global變成了local variable
而且在函數外定義了全局變量b=1,這個函數是用不了的
從生成的字節碼看下
>>> from dis import dis
>>> dis(func)
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 CALL_FUNCTION 1
6 POP_TOP
3 8 LOAD_GLOBAL 0 (print)
10 LOAD_FAST 1 (b) # 這里
12 CALL_FUNCTION 1
14 POP_TOP
4 16 LOAD_CONST 1 (1)
18 STORE_FAST 1 (b)
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
10 LOAD_FAST 1 (b)這一行暴露了b是個local variable
這不是缺陷,而是設計選擇:Python 不要求聲明變量,但是假定在函數定義體中賦值的變量是局部變量
這比 JavaScript 的行為好多了,JavaScript 也不要求聲明變量,但是如果忘記把變量聲明為局部變量(使用 var),可能會在不知情的情況下獲取全局變量
b = 6
def fun(a):
global b
print(a)
print(b)
b=9
fun(3)
print(b)
這個global必須要在fun中定義
此時的字節碼
12 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 CALL_FUNCTION 1
6 POP_TOP
13 8 LOAD_GLOBAL 0 (print)
10 LOAD_GLOBAL 1 (b)
12 CALL_FUNCTION 1
14 POP_TOP
14 16 LOAD_CONST 1 (9)
18 STORE_GLOBAL 1 (b)
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
7.5 閉包
人們有時會把閉包和匿名函數弄混。這是有歷史原因的:在函數內部定義函數不常見,直到開始使用匿名函數才會這樣做。而且,只有涉及嵌套函數時才有閉包問題。因此,很多人是同時知道這兩個概念的
閉包指延伸了作用域的函數,其中包含函數定義體中引用、但是不在定義體中定義的非全局變量。
函數是不是匿名的沒有關系,關鍵是它能訪問定義體之外定義的非全局變量
書中的一個例子,要實現類似下面的效果
它的作用是計算不斷增加的系列值的均值;例如,整個歷史中某個商品的平均收盤價。每天都會增加新價格,因此平均值要考慮至目前為止所有的價格
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
乍一看這個題,你肯定會想到這是個函數,這個函數傳入1個參數,內部有個東西可以記錄它的值,并計算出迄今為止所有數據的平均值
難道是這樣的?V1
def avg(value):
values = []
values.append(value)
return sum(values)/len(values)
print(avg(10))
print(avg(11))
顯然不對,每次調用的時候values會被重新初始化成[],所以始終就一個值
難道是這樣的?V2
values = []
def avg(value):
values.append(value)
return sum(values)/len(values)
print(avg(10))
print(avg(11))
print(avg(12))
竟然對了,但是這values不能在外面啊,你拿到外面去算啥嗎~
上面是我寫的,來看作者寫的
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)
avg = Averager()
print(avg(10))
print(avg(11))
print(avg(12))
看到avg()你指應該想到它是一個可調用對象,類實例也可以進行調用,實現__call__就行啦
那在類這里你要實現這個代碼就簡單了,上面的代碼應該可以想通,跟我們之前的蹩腳代碼異曲同工。
來看看函數式的實現:示例 7-9 average.py:計算移動平均值的高階函數
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
avg = make_averager() # 你得到的是 averager 函數名這個一等對象
print(avg(10)) # averager(10) 就是平均值
print(avg(11))
print(avg(12))
書中給出的類和函數的實現有共通之處:調用 Averager() 或 make_averager() 得到一個可調用對象avg,它會更新歷史值,然后計算當前均值
這個函數為何能進行累加呢?當然你能看得到這個寫法的特殊之處,函數里面有局部變量(series),又有內部函數averager。但注意這個內部函數用到了上面的局部變量
Averager 類的實例 avg 在哪里存儲歷史值很明顯:self.series 實例屬性。
但是第二個示例中的 avg 函數在哪里尋找 series 呢?
而且調用 avg(10) 時,make_averager 函數已經返回了,而它的本地作用域也一去不復返了。來看原文給的圖,我稍微擬合了下。
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
avg = make_averager()
avg(10)
avg(11)
avg(12)
# 審查返回的 averager 對象,我們發現 Python 在 __code__ 屬性(表示編譯后的函數定義體)中保存局部變量和*變量的名稱
# 局部變量
print(avg.__code__.co_varnames)
# *變量
print(avg.__code__.co_freevars)
# avg.__closure__ 中的各個元素對應于 avg.__code__.co_freevars 中的一個名稱。這些元素是 cell 對象,有個 cell_contents 屬性,保存著真正的值
print(avg.__closure__)
print(avg.__closure__[0].cell_contents)
輸出
('new_value', 'total') # 局部變量
('series',) #*變量
(<cell at 0x00000197FA6B4FD0: list object at 0x00000197FA083240>,)#包含該函數可用變量的綁定的單元的元組
[10, 11, 12] # 單元的值
這里再說明下這幾個屬性的作用
| 屬性 | 作用 |
|---|---|
| co_varnames | 參數名和局部變量的元組 |
| co_freevars | *變量的名字組成的元組(通過函數閉包引用) |
__closesure__ |
None 或包含該函數可用變量的綁定的單元的元組。有關 cell_contents 屬性的詳情見下。 |
| cell_contents | 單元對象具有 cell_contents 屬性。這可被用來獲取以及設置單元的值 |
引用自
https://docs.python.org/zh-cn/3.9/reference/datamodel.html?highlight=closure#the-standard-type-hierarchy
https://docs.python.org/zh-cn/3.9/library/inspect.html?highlight=inspect#module-inspect
avg.__closure__中的各個元素對應于avg.__code__.co_freevars中的一個名稱。這些元素是 cell 對象,有個 cell_contents 屬性,保存著真正的值
劃重點: 閉包是一種函數,它會保留定義函數時存在的*變量的綁定,這樣調用函數時,雖然定義作用域不可用了,但是仍能使用那些綁定
注意,只有嵌套在其他函數中的函數才可能需要處理不在全局作用域中的外部變量
7.6 nonlocal聲明
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
在上面的做法中
實現 make_averager 函數的方法效率不高
我們把所有值存儲在歷史數列中,然后在每次調用 averager 時使用 sum 求和
更好的實現方式是,只存儲目前的總值和元素個數,然后使用這兩個數計算均值
書中也給了個示例,但是個陷阱,你還能看出來問題所在?
def make_averager():
total = 0
count = 0
def averager(new_value):
total += new_value
count += 1
return count/length
return averager
avg = make_averager()
avg(10)
在Pycharm中定義函數就是紅色的警告,會提示類似未解析的引用 'count' ,里面三行都紅的。
但執行的時候會提示
Traceback (most recent call last):
File "demo_fluent7.py", line 10, in <module>
avg(10)
File "demo_fluent7.py", line 5, in averager
count += new_value
UnboundLocalError: local variable 'count' referenced before assignment
你上一次遇到它是在這里
>>> b = 1
>>> def func(a):
... print(a)
... print(b)
... b = 1
說明,這個count又成了一個局部變量?
看下dis
def make_averager():
...#省略
avg = make_averager()
from dis import dis
dis(avg)
輸出
5 0 LOAD_FAST 1 (count)
2 LOAD_FAST 0 (new_value)
4 INPLACE_ADD
6 STORE_FAST 1 (count)
6 8 LOAD_FAST 2 (total )
10 LOAD_CONST 1 (1)
12 INPLACE_ADD
14 STORE_FAST 2 (total )
7 16 LOAD_FAST 1 (count) # 看此處
18 LOAD_FAST 2 (total )
20 BINARY_TRUE_DIVIDE
22 RETURN_VALUE
為何會這樣呢?其實之前講過
當 count 是數字或任何不可變類型時,count += 1 語句的作用其實與 count =count + 1 一樣。因此,我們在 averager 的定義體中為 count 賦值了,
這會把 count 變成局部變量。total 變量也受這個問題影響。
當你寫series = []的時候,我們利用了列表是可變的對象這一事實,你在內部函數體中只是做了series.append,這個對象并沒有改變
但是對數字、字符串、元組等不可變類型來說,只能讀取,不能更新。如果嘗試重新綁定,例如 count = count + 1,其實會隱式創建局部變量 count。這樣,count 就不是*變量了,因此不會保存在閉包中
這個細節在本書《第2章 數據結構》的2.6 序列的增量賦值有描述,就是對數字而言,做count+=1的時候count不再是原來的count了
那是不是這樣的思路就不行了呢?倒也不是,就是稍微有點牽強
為了解決這個問題,Python 3 引入了 nonlocal 聲明。它的作用是把變量標記為*變量,即使在函數中為變量賦予新值了,也會變成*變量。如果為 nonlocal 聲明的變量賦予新值,閉包中保存的綁定會更新
最終可以替代前面的例子的代碼如下
def make_averager():
total = 0
count = 0
def averager(new_value):
nonlocal count,total
count += 1
total += new_value
return total/count
return averager
avg = make_averager()
print(avg(10))
print(avg(11))
print(avg(12))
在沒有實現nonlocal的情況下(比如Python2中)
http://www.python.org/dev/peps/pep-3104/
PEP 3104—Access to Names inOuter Scopes
其中的第三個代碼片段給出了一種方法。基本上,這種處理方式是把內部函數需要修改的變量(如 count 和 total)存儲為可變對象(如字典或簡單的實例)的元素或屬性,并且把那個對象綁定給一個*變量
7.7 實現一個簡單的裝飾器
示例 7-15 一個簡單的裝飾器,輸出函數的運行時間
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 get_time():
from time import sleep
sleep(2)
get_time()
? 定義內部函數 clocked,它接受任意個定位注:位置參數。
? 這行代碼可用,是因為 clocked 的閉包中包含*變量 func。
? 返回內部函數,取代被裝飾的函數
關于第2點,用代碼說明下
test = clock(get_time)
print(test.__code__.co_freevars) # ('func',)
示例 7-16 使用 clock 裝飾器
需要用到上面的代碼
import time
@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))
print(factorial.__name__)
執行效果
**************************************** Calling snooze(.123)
[0.12786180s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000050s] factorial(1) -> 1
[0.00000770s] factorial(2) -> 2
[0.00001190s] factorial(3) -> 6
[0.00001650s] factorial(4) -> 24
[0.00002100s] factorial(5) -> 120
[0.00002730s] factorial(6) -> 720
6! = 720
clocked
工作原理
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
等價于
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
factorial = clock(factorial)
factorial成為了clock的實參,指向func形參;調用后clock(factorial)返回的是clocked
看上面我加的調試代碼print(factorial.__name__)得到的就是clocked
現在 factorial 保存的是 clocked 函數的引用。自此之后,每次調用 factorial(n),執行的都是 clocked(n)。
代碼上clocked做了以下事情:
(1) 記錄初始時間 t0。 # t0 = time.perf_counter()
(2) 調用原來的 factorial 函數,保存結果。 # result = func(*args) # ?
(3) 計算經過的時間。 # elapsed = time.perf_counter() - t0
(4) 格式化收集的數據,然后打印出來。
# 收集的數據:包括前面的 elapsed 和 result
# name = func.__name__
# arg_str = ', '.join(repr(arg) for arg in args)
# print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
(5) 返回第 2 步保存的結果。 # return result
裝飾器的典型行為:
? 1. 把被裝飾的函數替換成新函數
? 2. 二者接受相同的參數
? 3. 而且(通常)返回被裝飾的函數本該返回的值
? 4. 同時還會做些額外操作
Gamma 等人寫的《設計模式:可復用面向對象軟件的基礎》一書是這樣概述“裝飾器”模式的:“動態地給一個對象添加一些額外的職責。”函數裝飾器符合這一說法。
但在實現層面,Python 裝飾器與《設計模式:可復用面向對象軟件的基礎》中所述的“裝飾器”沒有多少相似之處
示例 7-15 中實現的 clock 裝飾器有幾個缺點:不支持關鍵字參數,而且遮蓋了被裝飾函數的
__name__和__doc__屬性
import time
def clock(func):
'''doc of clock'''
def clocked(*args): # ?
'''doc of clocked'''
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):
'''doc of fact'''
return 1 if n < 2 else n*factorial(n-1)
if __name__=='__main__':
print(factorial.__doc__)
print(factorial.__name__)
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
snooze(seconds=.123)
注意上面的代碼,做了一些更改
- 加了factorial、clock和clocked等函數的doc,你可以看到,
print(factorial.__doc__)輸出的是clocked的doc - 測試了下關鍵字輸入方式
snooze(seconds=.123)提示如下
TypeError: clocked() got an unexpected keyword argument 'seconds'
如果要支持關鍵字只需做如下更改
def clock(func):
'''doc of clock'''
def clocked(*args,**kwargs): # ?
'''doc of clocked'''
t0 = time.perf_counter()
result = func(*args,**kwargs) # ?
? clocked本身要支持**kwargs
? 內部調用的時候要接受**kwargs
輸出大致如下
doc of clocked
clocked
**************************************** Calling snooze(.123)
[0.13518720s] snooze(0.123) -> None
[0.12407520s] snooze() -> None
問題1: 你可以看到,factorial的__doc__和__name__被遮擋了,這點在前面的萬能公式中我們也有提到
怎么處理呢?
使用 functools.wraps 裝飾器把相關的屬性從 func復制到 clocked 中
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args,**kwargs): # ?
t0 = time.perf_counter()
result = func(*args,**kwargs) # ?
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):
'''doc of fact'''
return 1 if n < 2 else n*factorial(n-1)
if __name__=='__main__':
print(factorial.__doc__) # doc of fact
print(factorial.__name__) # factorial
可以看到__doc__和__name__改過來了
問題2:snooze(seconds=.123)這種調用方式在結果中沒有輸出參數
原因很簡單,你沒有處理,你處理的只是args,你還要處理kwargs,參考代碼如下
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args,**kwargs): # ?
t0 = time.perf_counter()
result = func(*args,**kwargs) # ?
elapsed = time.perf_counter() - 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 # ?
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
'''doc of fact'''
return 1 if n < 2 else n*factorial(n-1)
if __name__=='__main__':
print(factorial.__doc__)
print(factorial.__name__)
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
snooze(seconds=.123)
7.8 標準庫中的裝飾器
Python 內置了三個用于裝飾方法的函數:property、classmethod 和 staticmethod
另一個常見的裝飾器是 functools.wraps,它的作用是協助構建行為良好的裝飾器
標 準 庫 中 最 值 得 關 注 的 兩 個 裝 飾 器 是 lru_cache 和 全 新 的singledispatch(Python 3.4 新增)
7.8.1 使用functools.lru_cache做備忘
functools.lru_cache 是非常實用的裝飾器,它實現了備忘(memoization)功能。這是一項優化技術,它把耗時的函數的結果保存起來,避免傳入相同的參數時重復計算。LRU 三個字母是“Least Recently Used”的縮寫,表明緩存不會無限制增長,一段時間不用的緩存條目會被扔掉
示例 7-18 生成第 n 個斐波納契數,遞歸方式非常耗時
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
if __name__=='__main__':
print(fibonacci(6))
這對你理解遞歸也是有幫助的
輸出如下(這個調用順序實在有點...)
[0.00000030s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00002610s] fibonacci(2) -> 1
[0.00000020s] fibonacci(1) -> 1
[0.00003430s] fibonacci(3) -> 2
[0.00000020s] fibonacci(1) -> 1
[0.00000020s] fibonacci(0) -> 0
[0.00000760s] fibonacci(2) -> 1
[0.00004960s] fibonacci(4) -> 3
[0.00000010s] fibonacci(1) -> 1
[0.00000020s] fibonacci(0) -> 0
[0.00000750s] fibonacci(2) -> 1
[0.00000020s] fibonacci(1) -> 1
[0.00001490s] fibonacci(3) -> 2
[0.00007280s] fibonacci(5) -> 5
[0.00000020s] fibonacci(1) -> 1
[0.00000020s] fibonacci(0) -> 0
[0.00000750s] fibonacci(2) -> 1
[0.00000010s] fibonacci(1) -> 1
[0.00001470s] fibonacci(3) -> 2
[0.00000020s] fibonacci(1) -> 1
[0.00000020s] fibonacci(0) -> 0
[0.00000750s] fibonacci(2) -> 1
[0.00002930s] fibonacci(4) -> 3
[0.00010970s] fibonacci(6) -> 8
8
畫個圖
從圖上可以看到,這里存在大量的重復操作
增加兩行代碼,使用 lru_cache,性能會顯著改善
@functools.lru_cache()
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
if __name__=='__main__':
print(fibonacci(6))
注意@functools.lru_cache()必須放@clock前面
這時候的輸出就是這樣,重復的調用都沒了
[0.00000040s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00002740s] fibonacci(2) -> 1
[0.00003230s] fibonacci(3) -> 2
[0.00003680s] fibonacci(4) -> 3
[0.00004120s] fibonacci(5) -> 5
[0.00004570s] fibonacci(6) -> 8
8
另外一個注意的點是:必須像常規函數那樣調用 lru_cache,后面有個()
作者做了個測試,可以看出,提升是巨大的
示例 7-19 中的版本(加了lru_cache的)在 0.0005 秒內調用了 31 次fibonacci 函數
示例 7-18 中未緩存版本調用 fibonacci 函數 2 692 537 次,在使用Intel Core i7 處理器的筆記本電腦中耗時 17.7 秒
lru_cache簽名
functools.lru_cache(maxsize=128, typed=False)
- maxsize指定存儲多少個調用的結果,緩存滿了之后,舊的結果會被扔掉,騰出空間。為了得到最佳性能,maxsize 應該設為 2 的冪
- typed 參數如果設為 True,把不同參數類型得到的結果分開保存,即把通常認為相等的浮點數和整數參數區分開
- lru_cache 使用字典存儲結果,而且鍵根據調用時傳入的定位參數和關鍵字參數創建,所以被 lru_cache 裝飾的函數,
它的所有參數都必須是可散列的,即不可變的
7.8.2 單分派泛函數
背景: 假設我們在開發一個調試 Web 應用的工具,我們想生成 HTML,顯示不同類型的 Python對象
import html
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
想改造這個函數,以期達到下面的效果
- str:把內部的換行符替換為
<br>\n';不使用<pre>,而是使用<p>。 - int:以十進制和十六進制顯示數字。
- list:輸出一個 HTML 列表,根據各個元素的類型進行格式化
示例 7-20 生成 HTML 的 htmlize 函數,調整了幾種對象的輸出
>>> htmlize({1, 2, 3}) ?
'<pre>{1, 2, 3}</pre>'
>>> htmlize(abs)
'<pre><built-in function abs></pre>'
>>> htmlize('Heimlich & Co.\n- a game') ?
'<p>Heimlich & Co.<br>\n- a game</p>'
>>> htmlize(42) ?
'<pre>42 (0x2a)</pre>'
>>> print(htmlize(['alpha', 66, {3, 2, 1}])) ?
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>
? 默認情況下,在 <pre></pre> 中顯示 HTML 轉義后的對象字符串表示形式。
? 為 str 對象顯示的也是 HTML 轉義后的字符串表示形式,不過放在 <p></p> 中,而且使用 <br> 表示換行。
? int 顯示為十進制和十六進制兩種形式,放在<pre></pre> 中。
? 各個列表項目根據各自的類型格式化,整個列表則渲染成 HTML 列表。
因為 Python 不支持重載方法或函數,所以我們不能使用不同的簽名定義 htmlize 的變體,也無法使用不同的方式處理不同的數據類型。
重載overload,java中可以。Python只有override(重寫)。
不同的簽名是不支持的
def htmlize(obj:int):
pass
def htmlize(obj:str):
pass
def htmlize(obj:list):
pass
但我知道的一種做法是可以用第三方庫,類似于這樣
from multipledispatch import dispatch
@dispatch(int)
def htmlize(obj):
print('int')
@dispatch(str)
def htmlize(obj):
print('str')
@dispatch(list)
def htmlize(obj):
print('list')
htmlize(1) # int
htmlize('1') # str
htmlize([1,]) # list
htmlize((1,)) # 報錯
# NotImplementedError: Could not find signature for htmlize: <tuple>
書中還說了一句也無法使用不同的方式處理不同的數據類型,我沒太理解,不是可以用isinstance來處理嗎?莫非在寫的時候
沒有這個玩意
第二版英文原文如下
Because we don’t have Java-style method overloading in Python, we can’t simply cre‐ate variations of htmlize with different signatures for each data type we want to han‐dle differently
在 Python 中,一種常見的做法是把 htmlize變成一個分派函數,使用一串 if/elif/elif,調用專門的函數,如 htmlize_str、htmlize_int,等等。這樣不便于模塊的用戶擴展,還顯得笨拙:時間一長,分派函數 htmlize 會變
得很大,而且它與各個專門函數之間的耦合也很緊密
書中給出的示例 7-21 singledispatch 創建一個自定義的 htmlize.register 裝飾器,把多個函數綁在一起組成一個泛函數
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>'
# 測試數據
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}]))
? @singledispatch 標記處理 object 類型的基函數。
? 各個專門函數使用 @?base_function?.register(?type?) 裝飾。
? 專門函數的名稱無關緊要;_ 是個不錯的選擇,簡單明了。
? 為每個需要特殊處理的類型注冊一個函數。numbers.Integral 是 int 的虛擬超類。
? 可以疊放多個 register 裝飾器,讓同一個函數支持不同類型。
只要可能,注冊的專門函數應該處理抽象基類(如 numbers.Integral 和 abc.MutableSequence),不要處理具體實現(如 int 和 list)。這樣,代碼支持的兼容類型更廣泛。例如,Python擴展可以子類化 numbers.Integral,使用固定的位數實現 int 類型
使用抽象基類檢查類型,可以讓代碼支持這些抽象基類現有和未來的具體子類或虛擬子類
singledispatch 機制的一個顯著特征是,你可以在系統的任何地方和任何模塊中注冊專門函數。
如果后來在新的模塊中定義了新的類型,可以輕松地添加一個新的專門函數來處理那個類型。
此外,你還可以為不是自己編寫的或者不能修改的類添加自定義函數。
singledispatch 是經過深思熟慮之后才添加到標準庫中的,它提供的特性很多 , 詳見
PEP 443 — Single-dispatch generic functions
https://www.python.org/dev/peps/pep-0443/
@singledispatch 不是為了把 Java 的那種方法重載帶入 Python。在一個類中為同一個方法定義多個重載變體,比在一個函數中使用一長串 if/elif/elif/elif 塊要更好。但是這兩種方案都有缺陷,因為它們讓代碼單元(類
或函數)承擔的職責太多。@singledispath 的優點
是支持模塊化擴展:各個模塊可以為它支持的各個類型注冊一個專門函數
7.9 疊放裝飾器
裝飾器是函數,因此可以組合起來使用(即,可以在已經被裝飾的函數上應用裝飾器)
前面已經多次這樣使用,比如
@functools.lru_cache()
@clock
def fibonacci(n):
pass
但要注意順序
@d1
@d2
def f():
print('f')
等價于f = d1(d2(f)),就近原則,最近@的最先裝飾
7.10 參數化裝飾器
解析源碼中的裝飾器時,Python 把被裝飾的函數作為第一個參數傳給裝飾器函數。
那怎么讓裝飾器接受其他參數呢?
答案是:創建一個裝飾器工廠函數,把參數傳給它,返回一個裝飾器,然后再把它應用到要裝飾的函數上
書中給了個示例示例 7-22 示例 7-2 中 registration.py 模塊的刪減版
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()
7.10.1 一個參數化的注冊裝飾器
為了便于啟用或禁用 register 執行的函數注冊功能,我們為它提供一個可選的 active 參數,設為 False 時,不注冊被裝飾的函數
從概念上看,這個新的 register 函數不是裝飾器,而是裝飾器工廠函數。調用它會返回真正的裝飾器,這才是應用到目標函數上的裝飾器。
示例 7-23 為了接受參數,新的 register 裝飾器必須作為函數調用
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()')
? registry 現在是一個 set 對象,這樣添加和刪除函數的速度更快。
? register 接受一個可選的關鍵字參數。
? decorate 這個內部函數是真正的裝飾器;注意,它的參數是一個函數。
? 只有 active 參數的值(從閉包中獲取)是 True 時才注冊 func。
? 如果 active 不為真,而且 func 在 registry 中,那么把它刪除。
? decorate 是裝飾器,必須返回一個函數。
? register 是裝飾器工廠函數,因此返回 decorate。然后把它應用到被裝飾的函數上
? @register 工廠函數必須作為函數調用,并且傳入所需的參數。
? 即使不傳入參數,register 也必須作為函數調用(@register()),即要返回真正的裝飾器 decorate
在終端下你可以測試出以下結果,假設文件是demo.py
>>> import demo
running register(active=False)->decorate(<function f1 at 0x000002860CF2CEE0>)
running register(active=True)->decorate(<function f2 at 0x000002860CF2CF70>)
>>> demo.registry
{<function f2 at 0x000002860CF2CF70>}
跟之前7.2說的一樣導入的時候就會執行
只有 f2 函數在 registry 中;f1 不在其中,因為傳給 register 裝飾器工廠函數的參數是 active=False,所以應用到 f1 上的 decorate 沒有把它添加到 registry 中
如果不使用 @ 句法,那就要像常規函數那樣使用 register;若想把 f 添加到 registry中,則裝飾 f 函數的句法是 register()(f);不想添加(或把它刪除)的話,句法是register(active=False)(f)
上面這部分是關鍵
@clock
def fibonacci(n):
pass
你知道,fibonacci=clock(fibonacci)
那你現在要做的是
@clock(param='xxx')
def fibonacci(n):
pass
那自然fibonacci=clock(param='xxx')(fibonacci)
所以你應該定義一個
def clock(param='xxx'):
pass
而這個clock的返回需要是一個函數,參數應該是一個函數(比如fibonacci)
def clock(param='xxx'):
def decorate(func):
pass
decorate
書中還給你做了下如下測試
>>> from registration_param import * # 我上面的測試改了此處的名字 demo
running register(active=False)->decorate(<function f1 at 0x10073c1e0>)
running register(active=True)->decorate(<function f2 at 0x10073c268>)
>>> registry # ?
{<function f2 at 0x10073c268>}
>>> register()(f3) # ?
running register(active=True)->decorate(<function f3 at 0x10073c158>)
<function f3 at 0x10073c158>
>>> registry # ?
{<function f3 at 0x10073c158>, <function f2 at 0x10073c268>}
>>> register(active=False)(f2) # ?
running register(active=False)->decorate(<function f2 at 0x10073c268>)
<function f2 at 0x10073c268>
>>> registry # ?
{<function f3 at 0x10073c158>}
? 導入這個模塊時,f2 在 registry 中。
? register() 表達式返回 decorate,然后把它應用到 f3 上。
? 前一行把 f3 添加到 registry 中。
? 這次調用從 registry 中刪除 f2。
? 確認 registry 中只有 f3。
7.10.2 參數化clock裝飾器
上面的裝飾器比較簡單,但通常參數化裝飾器的原理相當復雜,
參數化裝飾器通常會把被裝飾的函數替換掉,而且結構上需要多一層嵌套。
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_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())) # ?
return _result # ?
return clocked # ?
return decorate # ?
? clock 是參數化裝飾器工廠函數。
? decorate 是真正的裝飾器。
? clocked 包裝被裝飾的函數。
? _result 是被裝飾的函數返回的真正結果。
? _args 是 clocked 的參數,args 是用于顯示的字符串。
? result 是 _result 的字符串表示形式,用于顯示。
? 這里使用**locals()是為了在 fmt 中引用 clocked 的局部變量。
? clocked 會取代被裝飾的函數,因此它應該返回被裝飾的函數返回的值。
? decorate 返回 clocked。
? clock 返回 decorate。在這個模塊中測試,不傳入參數調用 clock(),因此應用的裝飾器使用默認的格式 str。應該是DEFAULT_FMT
**locals()** 函數會以字典類型返回當前位置的全部局部變量,配合fmt來用,還是挺巧妙的~
locals: {'_args': (0.123,), 't0': 1699234406.3928096, '_result': None, 'elapsed': 0.12681794166564941, 'name': 'snooze', 'args': '0.123', 'result': 'None', 'fmt': '[{elapsed:0.8f}s] {name}({args}) -> {result}', 'func': <function snooze at 0x0000026ED4107F70>}
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}' # 在上面也有
另外一點就是
參數化裝飾器通常會把被裝飾的函數替換掉,而且結構上需要多一層嵌套。
考慮上面的結構
def clock(fmt=DEFAULT_FMT):
def decorate(func):
def clocked(*_args):
...
return _result
return clocked
return decorate
@clock()
def snooze(seconds):
pass
結合萬能公式
snooze=clock()(snooze) #注意此處的第一個()
snooze=decorate(snooze) # 轉換下
snooze=clocked # 替換了
最終
for i in range(3):
snooze(.123)
就相當于
for i in range(3):
clocked(.123)
所以下面的幾個測試結果
測試1
if __name__ == '__main__':
@clock()
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
輸出
[0.13555145s] snooze(0.123) -> None
[0.12589598s] snooze(0.123) -> None
[0.12798786s] snooze(0.123) -> None
測試2
if __name__ == '__main__':
@clock('{name}: {elapsed}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
輸出
snooze: 0.12915396690368652s
snooze: 0.1259920597076416s
snooze: 0.1258389949798584s
測試3
if __name__ == '__main__':
@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
輸出
snooze(0.123) dt=0.126s
snooze(0.123) dt=0.126s
snooze(0.123) dt=0.126s
Graham Dumpleton 和 Lennart Regebro(本書的技術審校之一)認為,
裝飾器最好通過實現 __call__ 方法的類實現,不應該像本章的示例那樣通過函數實現
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
class Clock:
def __init__(self,fmt=DEFAULT_FMT):
self.fmt = fmt
def __call__(self, 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('locals:',locals())
print(self.fmt.format(**locals()))
return _result
return clocked
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)
同樣的推導
snooze=Clock()(snooze)
其中Clock()是個實例,假設為clock_instance
那clock_instance(snnoze)就是在調用__call__,返回的就是clocked,也發生了替換
從寫法上更讓清晰一些
7.11 本章小結
從本章開始進入元編程領域
開始,我們先編寫了一個沒有內部函數的 @register 裝飾器;最后,我們實現了有兩層嵌套函數的參數化裝飾器 @clock()
參數化裝飾器基本上都涉及至少兩層嵌套函數,如果想使用 @functools.wraps 生成裝飾器,為高級技術提供更好的支持,嵌套層級可能還會更深,比如前面簡要介紹過的疊放裝飾器
討論了標準庫中 functools 模塊提供的兩個出色的函數裝飾器:@lru_cache() 和@singledispatch
若想真正理解裝飾器,需要區分導入時和運行時,還要知道變量作用域、閉包和新增的nonlocal 聲明。掌握閉包和 nonlocal 不僅對構建裝飾器有幫助,還能協助你在構建 GUI程序時面向事件編程,或者使用回調處理異步 I/O
7.12 延伸閱讀
| 素材 | URL | 相關信息 |
|---|---|---|
| Python Cookbook(第 3 版)中文版》第 9 章“元編程” | 有幾個訣竅構建了基本的裝飾器和特別復雜的裝飾器 9.6 定義一個能接收可選參數的裝飾器”一節中的裝飾器可以作為常規的裝飾器調用,也可以作為裝飾器工廠函數調用,例如 @clock 或 @clock() |
|
| Graham Dumpleton 博 客 文 章 | https://github.com/GrahamDumpleton/wrapt/blob/develop/blog/README.md | 深入剖析了如何實現行為良好的裝飾器 |
| How You Implemented Your Python Decorator is Wrong | https://github.com/GrahamDumpleton/wrapt/blob/develop/blog/01-how-you-implemented-your-python-decorator-is-wrong.md | |
| wrapt 模塊 | http://wrapt.readthedocs.org/en/latest | 這個模塊的作用是簡化裝飾器和動態函數包裝器的實現,即使多層裝飾也支持內省,而且行為正確,既可以應用到方法上,也可以作為描述符使用 |
| Michele Simionato的decorator包 | https://pypi.python.org/pypi/decorator | 簡化普通程序員使用裝飾器的方式,并且通過各種復雜的示例推廣裝飾器 |
| Python Decorator Library 維基頁面 | https://wiki.python.org/moin/PythonDecoratorLibrary | 里面有很多示例 |
| PEP 443 | http://www.python.org/dev/peps/pep-0443 | 對單分派泛函數的基本原理和細節做了說明 |
| Five-Minute Multimethods in Python | http://www.artima.com/weblogs/viewpost.jsp?thread=101605 | 詳細說明了如何使用裝飾器實現泛函數(也叫多方法),他給出的代碼支持多分派(即根據多個定位參數進行分派) |
| Martijn Faassen 開發的 Reg | http://reg.readthedocs.io/en/latest/ | 如果想使用現代 的技術實現多分派泛函數,并支持在生產環境中使用,可以用 它 |
| Fredrik Lundh 寫的一篇短文Closures in Python | http://effbot.org/zone/closure.htm | 解說了閉包這個術語 |
| PEP 3104—Access to Names in Outer Scopes | http://www.python.org/dev/peps/pep-3104 | 說明了引入 nonlocal 聲明的原因:重新綁定既不在本地作用域中也不在全局作用域中的名稱。這份 PEP 還概述了其他動態語言(Perl、Ruby、JavaScript,等等)解決這個問題的方式,以及 Python 中可用設計方案的優缺點 |
| PEP 227—Statically Nested Scopes | http://www.python.org/dev/peps/pep-0227/ | 說明了 Python 2.1 引入的詞法作用域;這份 PEP 還說明了 Python 中閉包的基本原理和實現方式的選擇 |
雜談
-
任何把函數當作一等對象的語言,它的設計者都要面對一個問題:作為一等對象的函數在某個作用域中定義,但是可能會在其他作用域中調用。問題是,如何計算*變量?首先出現的最簡單的處理方式是使用“動態作用域”。也就是說,根據函數調用所在的環境計算*變量。
-
動態作用域易于實現,這可能就是 John McCarthy 創建 Lisp(第一門把函數視作一等對象的語言)時采用這種方式的原因
-
Python 函數裝飾器符合 Gamma 等人在《設計模式:可復用面向對象軟件的基礎》一書中對“裝飾器”模式的一般描述:“動態地給一個對象添加一些額外的職責。就擴展功能而言,裝飾器模式比子類化更靈活。”
-
在設計模式中,Decorator 和 Component 是抽象類。為了給具體組件添加行為,具體裝飾器的實例要包裝具體組件的實例
-
裝飾器與它所裝飾的組件接口一致,因此它對使用該組件的客戶透明。它將客戶請求轉發給該組件,并且可能在轉發前后執行一些額外的操作(例如繪制一個邊框)。透明性使得你可以遞歸嵌套多個裝飾器,從而可以添加任意多的功能
-
一般來說,實現“裝飾器”模式時最好使用類表示裝飾器和要包裝的組件
還有很多,不再一一羅列了啦,雜談部分就當看Python歷史了
關于裝飾器的一個典型應用
引自 劉江的博客
有一個大公司,下屬的基礎平臺部負責內部應用程序及API的開發。另外還有上百個業務部門負責不同的業務,這些業務部門各自調用基礎平臺部提供的不同函數,也就是API處理自己的業務,情況如下:
# 基礎平臺部門開發了上百個函數API
def f1():
print("業務部門1的數據接口......")
def f2():
print("業務部門2的數據接口......")
def f3():
print("業務部門3的數據接口......")
def f100():
print("業務部門100的數據接口......")
#各部門分別調用自己需要的API
f1()
f2()
f3()
f100()
公司還在創業初期時,基礎平臺部就開發了這些函數。由于各種原因,比如時間緊,比如人手不足,比如架構缺陷,比如考慮不周等等,沒有為函數的調用進行安全認證。現在,公司發展壯大了,不能再像初創時期的“草臺班子”一樣將就下去了,基礎平臺部主管決定彌補這個缺陷,于是(以下場景純屬虛構,調侃之言,切勿對號入座):
第一天:主管叫來了一個運維工程師,工程師跑上跑下逐個部門進行通知,讓他們在代碼里加上認證功能,然后,當天他被開除了。
第二天:主管又叫來了一個運維工程師,工程師用shell寫了個復雜的腳本,勉強實現了功能。但他很快就回去接著做運維了,不會開發的運維不是好運維....
第三天:主管叫來了一個python自動化開發工程師。哥們是這么干的,只對基礎平臺的代碼進行重構,讓N個業務部門無需做任何修改。這哥們很快也被開了,連運維也沒得做。
def f1():
#加入認證程序代碼
print("業務部門1數據接口......")
def f2():
# 加入認證程序代碼
print("業務部門2數據接口......")
def f3():
# 加入認證程序代碼
print("業務部門3數據接口......")
def f100():
#加入認證程序代碼
print("業務部門100數據接口......")
#各部門分別調用
f1()
f2()
f3()
f100()
第四天:主管又換了個開發工程師。他是這么干的:定義個認證函數,在原來其他的函數中調用它,代碼如下。
def login():
print("認證成功!")
def f1():
login()
print("業務部門1數據接口......")
def f2():
login()
print("業務部門2數據接口......")
def f3():
login()
print("業務部門3數據接口......")
def f100():
login()
print("業務部門100數據接口......")
#各部門分別調用
f1()
f2()
f3()
f100()
但是主管依然不滿意,不過這一次他解釋了為什么。主管說:寫代碼要遵循開放封閉原則,簡單來說,已經實現的功能代碼內部不允許被修改,但外部可以被擴展。如果將開放封閉原則應用在上面的需求中,那么就是不允許在函數f1 、f2、f3......f100的內部進行代碼修改,但是可以在外部對它們進行擴展。
第五天:已經沒有時間讓主管找別人來干這活了,他決定親自上陣,使用裝飾器完成這一任務,并且打算在函數執行后再增加個日志功能。主管的代碼如下:
def outer(func):
def inner():
print("認證成功!")
result = func()
print("日志添加成功")
return result
return inner
@outer
def f1():
print("業務部門1數據接口......")
@outer
def f2():
print("業務部門2數據接口......")
@outer
def f3():
print("業務部門3數據接口......")
@outer
def f100():
print("業務部門100數據接口......")
#各部門分別調用
f1()
f2()
f3()
f100()
使用裝飾器@outer,也是僅需對基礎平臺的代碼進行拓展,就可以實現在其他部門調用函數API之前都進行認證操作,在操作結束后保存日志,并且其他業務部門無需對他們自己的代碼做任何修改,調用方式也不用變
總結
以上是生活随笔為你收集整理的《流畅的Python》 读书笔记 第7章_函数装饰器和闭包的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ubuntu部署雷池Waf社区版
- 下一篇: 阿里Java一面,难度适中!(下篇)