摘要:函數裝飾器和閉包嚴格來說,裝飾器只是語法糖。何時執行裝飾器它們在被裝飾的函數定義之后立即運行。裝飾器突出了被裝飾的函數的作用,還便于臨時禁用某個促銷策略只需把裝飾器注釋掉。
函數裝飾器和閉包
嚴格來說,裝飾器只是語法糖。如前所示,裝飾器可以像常規的可調用
對象那樣調用,其參數是另一個函數。有時,這樣做更方便,尤其是做
元編程(在運行時改變程序的行為)時。
它們在被裝飾的函數定義之后立即運行。這通常是在導入時(即 Python 加載模塊時)
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()
把 registration.py 當作腳本運行得到的輸出如下:
$ python3 registration.py running register(如果導入 registration.py 模塊(不作為腳本運行),輸出如下:) running register( ) running main() registry -> [ , ] running f1() running f2() running f3()
>>> import registration running register() running register( )
此時查看 registry 的值,得到的輸出如下:
>>> registration.registry [裝飾器在真實代碼中的常用方式, ]
裝飾器函數與被裝飾的函數在同一個模塊中定義。實際情況是,裝
飾器通常在一個模塊中定義,然后應用到其他模塊中的函數上。
promos = [] def promotion(promo_func): promos.append(promo_func) return @promotion def fidelity(order): """為積分為1000或以上的顧客提供5%折扣""" return order.total() * .05 if order.customer.fidelity >= 1000 else 0 @promotion def bulk_item(order): """單個商品為20個或以上時提供10%折扣""" discount = 0 for item in order.cart: if item.quantity >= 20: discount += item.total() * .1 return discount @promotion def large_order(order): """訂單中的不同商品達到10個或以上時提供7%折扣""" distinct_items = {item.product for item in order.cart} if len(distinct_items) >= 10: return order.total() * .07 return 0 def best_promo(order): """選擇可用的最佳折扣""" return max(promo(order) for promo in promos)
promotion 把 promo_func 添加到 promos 列表中,然后原封不動地將其返回。
被 @promotion 裝飾的函數都會添加到 promos 列表中。
與 6.1 節給出的方案相比,這個方案有幾個優點。促銷策略函數無需使用特殊的名稱(即不用以 _promo 結尾)。
@promotion 裝飾器突出了被裝飾的函數的作用,還便于臨時禁用
某個促銷策略:只需把裝飾器注釋掉。
促銷折扣策略可以在其他模塊中定義,在系統中的任何地方都行,只要使用 @promotion 裝飾即可。
變量作用域規則神奇的例子
>>> b = 6 >>> def f2(a): ... print(a) ... print(b) ... b = 9 ... >>> f2(3) 3 Traceback (most recent call last): File "", line 1, in File " ", line 3, in f2 UnboundLocalError: local variable "b" referenced before assignment
可事實是,Python 編譯函數的定義體時,它判斷 b 是局部變量,因為在函數中給它賦值了。
生成的字節碼證實了這種判斷,Python 會嘗試從本地環境獲取 b。
后面調用 f2(3) 時, f2 的定義體會獲取并打印局部變量 a 的值,但是嘗試獲取局部變量 b 的值時,發現 b 沒有綁定值。
為什么會這樣這不是缺陷,而是設計選擇:Python 不要求聲明變量,但是假定在函數定義體中賦值的變量是局部變量。
這比 JavaScript 的行為好多了,JavaScript 也不要求聲明變量,但是如果忘記把變量聲明為局部變量使用 var),可能會在不知情的情況下獲取全局變量。
利用global就可以啦
>>> b = 6 >>> def f3(a): ... global b ... print(a) ... print(b) ... b = 9 ... >>> f3(3) 3 6閉包
人們有時會把閉包和匿名函數弄混。
這是有歷史原因的:在函數內部定義函數不常見,直到開始使用匿名函數才會這樣做,
函數是不是匿名的沒有關系,關鍵是它能訪問定義體之外定義的非全局變量。
只有涉及嵌套函數時才有閉包問題。因此,很多人是同時知道這兩個概念的。
案例假如有個名為 avg 的函數,它的作用是計算不斷增加的系列值的均值;
初學者可能會這樣
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() >>> avg(10) 10.0 >>> avg(11) 10.5 >>> avg(12) 11.0函數式實現,使用高階函數 make_averager。
def make_averager(): series = [] def averager(new_value): series.append(new_value) total = sum(series) return total / len(series) return averager重要概念
在 averager 函數中,series 是自由變量(free variable)。這是一個
技術術語,指未在本地作用域中綁定的變量.
>>> avg.__code__.co_varnames ("new_value", "total") >>> avg.__code__.co_freevars ("series",)
series 的綁定在返回的 avg 函數的 closure 屬性中。
avg.__closure__ 中的各個元素對應于avg.__code__.co_freevars 中的一個名稱。
這些元素是 cell 對象,有個 cell_contents 屬性,保存著真正的值。
>>> avg.__code__.co_freevars ("series",) >>> avg.__closure__ (小總結,) >>> avg.__closure__[0].cell_contents [10, 11, 12] |
綜上,閉包是一種函數,它會保留定義函數時存在的自由變量的綁定,
這樣調用函數時,雖然定義作用域不可用了,但是仍能使用那些綁定。
注意,只有嵌套在其他函數中的函數才可能需要處理不在全局作用域中的外部變量。
nonlocal聲明計算移動平均值的高階函數,不保存所有歷史值,但有
缺陷
def make_averager(): count = 0 total = 0 def averager(new_value): count += 1 total += new_value return total / count return averager
問題是,當 count 是數字或任何不可變類型時
count += 1 語句的作用其實與 count = count + 1 一樣。注意
因此,我們在 averager 的定義體中為 count 賦值了,這會把 count 變成局部變量。
示例 7-9 沒遇到這個問題,因為我們沒有給 series 賦值,我們只是調
用 series.append,并把它傳給 sum 和 len。也就是說,我們利用了
列表是可變的對象這一事實。但是對數字、字符串、元組等不可變類型來說,只能讀取,不能更新。
如果嘗試重新綁定,例如 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對付沒有 nonlocal 的 Python 2
基本上,這種處理方式是把內部函數需要修改實現一個簡單的裝飾器
的變量(如 count 和 total)存儲為可變對象(如字典或簡單的
實例)的元素或屬性,并且把那個對象綁定給一個自由變量。
import time def clock(func): def clocked(*args): t0 = time.perf_counter() result = func(*args) elapsed = time.perf_counter() - t0 name = func.__name__ args_str = "".join(repr(arg) for arg in args) print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, args_str, result)) return result return clocked @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) snooze(0.123) print("*" * 40) factorial(6) ## 這里的函數對象變成了從clocked print(factorial.__name__)這是裝飾器的典型行為:把被裝飾的函數替換成新函數,二者接受相同的參數,而且(通
常)返回被裝飾的函數本該返回的值,同時還會做些額外操作。
上面實現的 clock 裝飾器有幾個缺點:不支持關鍵字參數,而且遮蓋了被裝飾函
數的 name 和 doc 屬性。使用 functools.wraps 裝飾器把相關的屬性從 func 復制到 clocked 中。此外,這個新版還能正確處理關鍵字參數。
import time import functools def clock(func): @functools.wraps(func) ###這里 保留__name__ 和 __doc__ 屬性 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)功能。
就是更加利用緩存干活
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 @clock def fibonacci(n): if n < 2: return n return fibonacci(n - 2) + fibonacci(n - 1) if __name__ == "__main__": print(fibonacci(6))
浪費時間的地方很明顯:fibonacci(1) 調用了 8 次,fibonacci(2) 調用了 5 次……但是,如果增加兩行代碼,使用 lru_cache,性能會顯著改善,
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 @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))
? 注意,必須像常規函數那樣調用 lru_cache。這一行中lru_cache 可以使用兩個可選的參數來配置。
有一對括
號:@functools.lru_cache()。這么做的原因是,lru_cache 可以接受配置參數,稍
后說明。
maxsize 參數指定存儲多少個調用的結果。
緩存滿了之后,舊的結果會被扔掉,騰出空間。
為了得到最佳性能,maxsize 應該設為 2 的冪。typed 參數如果設為 True,把不同
參數類型得到的結果分開保存,即把通常認為相等的浮點數和整數參數(如 1 和 1.0)區
分開。
functools.singledispatch 裝飾器 讓Python強行支持重載方法個函數綁在一起組成一個泛函數
from functools import singledispatch from collections import abc import numbers import html @singledispatch def htmlize(obj): content = html.escape(repr(obj)) return "{}".format(content) @htmlize.register(str) def _(text): content = html.escape(text).replace(" ", "
") return "{0}
".format(content) @htmlize.register(numbers.Integral) def _(n): return "{0} (0x{0:x})".format(n) @htmlize.register(tuple) @htmlize.register(abc.MutableSequence) def _(seq): inner = "
? 各個專門函數使用 @?base_function?.register(?type?) 裝飾。
? 專門函數的名稱無關緊要;_ 是個不錯的選擇,簡單明了。
為每個需要特殊處理的類型注冊一個函數。numbers.Integral 是 int 的虛擬超類。
? 可以疊放多個 register 裝飾器,讓同一個函數支持不同類型。
注意:只要可能,注冊的專門函數應該處理抽象基類(如 numbers.Integral 和
abc.MutableSequence),不要處理具體實現(如 int 和 list)。
這樣,代碼支持的兼容類型更廣泛。例如,Python 擴展可以子類化 numbers.Integral,使用固定的位數實
現 int 類型。
@singledispatch 不是為了把 Java 的那種方法重載帶入 Python。在一個類中 為同一個方法定義多個重載變體,
@singledispath 的優點是支持模塊化擴展:各個模塊可以為它支持的各個類型注冊一個專門函數。
疊放裝飾器@d1 @d2 def f(): print("f")
等同于
def f(): print("f") f = d1(d2(f))一個參數化的注冊裝飾器
為了便于啟用或禁用 register 執行的函數注冊功能,我們為它提供一個可選的 active
參數,設為 False 時,不注冊被裝飾的函數。
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()") if __name__ =="__main__": print(registry)
參數化裝飾器通常會把被裝飾的函數替換掉,而且結構上需要多一層嵌套。參數化clock裝飾器
import time DEFAULT_FMT = "花費時間:[{elapsed:0.5f}s] 程序名:{name} 參數:({args}) -> 結果:{result}" def clock(fmt=DEFAULT_FMT): def decorate(func): def clocked(*_args): t0 = time.time() _result = func(*_args) ### locals() 局部變量 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 if __name__ == "__main__": # ## 第一種情況 # @clock() # def snooze(seconds): # time.sleep(seconds) ## 第二種情況 # @clock("程序名:{name}: 花費時間:{elapsed}s") # def snooze(seconds): # time.sleep(seconds) ## 第三種情況 @clock("程序名:{name} 參數:({args}) 花費時間:dt={elapsed:0.3f}s") def snooze(seconds): time.sleep(seconds) snooze(0.123)
clock 是參數化裝飾器工廠函數
? decorate 是真正的裝飾器。
? clocked 包裝被裝飾的函數。
? _result 是被裝飾的函數返回的真正結果
def runnoob(arg:"int"): z = 1 print(arg + 1) # 返回字典類型的局部變量。 print("==="*30) print(locals()) # 返回字典類型的全部變量。 print("=" * 50) print(globals()) num = 8 runnoob(num)小總結
嚴格來說,裝飾器只是語法糖。
它們在被裝飾的函數定義之后立即運行。這通常是在導入時(即 Python 加載模塊時)
裝飾器改進了策略模式
閉包閉包是一種函數,它會保留定義函數時存在的自由變量的綁定
Python 3 引入了 nonlocal 聲明。它的作用是把變
量標記為自由變量
functools.wraps 裝飾器把相關的屬性從 func 復制到 clocked 中。此外,這個新版還能正確處理關鍵字參數
functools.lru_cache 是非常實用的裝飾器,它實現了備忘(memoization)功能。就是更加利用緩存干活 functools.singledispatch 裝飾器 讓Python強行支持重載方法 locals() globals()*locals
** locals()
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/41655.html
摘要:初步認識裝飾器函數裝飾器用于在源代碼中標記函數,以某種方式增強函數的行為。函數裝飾器在導入模塊時立即執行,而被裝飾的函數只在明確調用時運行。只有涉及嵌套函數時才有閉包問題。如果想保留函數原本的屬性,可以使用標準庫中的裝飾器。 《流暢的Python》筆記本篇將從最簡單的裝飾器開始,逐漸深入到閉包的概念,然后實現參數化裝飾器,最后介紹標準庫中常用的裝飾器。 1. 初步認識裝飾器 函數裝飾...
摘要:可迭代的對象迭代器和生成器理念迭代是數據處理的基石。可迭代的對象與迭代器的對比從可迭代的對象中獲取迭代器標準的迭代器接口有兩個方法。此外,也沒有辦法還原迭代器。最終,函數的定義體返回時,外層的生成器對象會拋出異常這一點與迭代器協議一致。 可迭代的對象、迭代器和生成器 理念 迭代是數據處理的基石。掃描內存中放不下的數據集時,我們要找到一種惰性獲取數據項的方式,即按需一次獲取一個數據項。這...
摘要:變量查找規則在中一個變量的查找順序是局部環境,閉包,全局,內建閉包引用了自由變量的函數。閉包的作用閉包的最大特點是可以將父函數的變量與內部函數綁定,并返回綁定變量后的函數,此時即便生成閉包的環境父函數已經釋放,閉包仍然存在。 導語:本文章記錄了本人在學習Python基礎之函數篇的重點知識及個人心得,打算入門Python的朋友們可以來一起學習并交流。 本文重點: 1、掌握裝飾器的本質、功...
摘要:第六章抽象本章會介紹如何將語句組織成函數。關鍵字參數和默認值目前為止,我們使用的參數都是位置參數,因為它們的位置很重要,事實上比它們的名字更重要。參數前的星號將所有值放置在同一個元祖中。函數內的變量被稱為局部變量。 第六章:抽象 本章會介紹如何將語句組織成函數。還會詳細介紹參數(parameter)和作用域(scope)的概念,以及遞歸的概念及其在程序中的用途。 懶惰即美德 斐波那契數...
閱讀 2990·2023-04-26 00:23
閱讀 3406·2021-09-13 10:28
閱讀 2185·2021-08-31 14:18
閱讀 2891·2019-08-30 15:54
閱讀 1945·2019-08-30 15:43
閱讀 1284·2019-08-29 16:56
閱讀 2807·2019-08-29 14:16
閱讀 2060·2019-08-28 17:51