'不懂裝飾器,就不是真正會 Python'

Python 面向對象程序編程 編程派 2019-08-29
""不懂裝飾器,就不是真正會 Python

文 | piglei@piglei 公眾號

編輯 | EarlGrey

推薦 | 編程派公眾號

裝飾器(Decorator) 是 Python 裡的一種特殊工具,它為我們提供了一種在函數外部修改函數的靈活能力。它有點像一頂畫著獨一無二 @符號的神奇帽子,只要將它戴在函數頭頂上,就能悄無聲息的改變函數本身的行為。

你可能已經和裝飾器打過不少交道了。在做面向對象編程時,我們就經常會用到 @staticmethod@classmethod兩個內置裝飾器。此外,如果你接觸過 click 模塊,就更不會對裝飾器感到陌生。click 最為人所稱道的參數定義接口@click.option(...)就是利用裝飾器實現的。

除了用裝飾器,我們也經常需要自己寫一些裝飾器。在這篇文章裡,我將從 最佳實踐常見錯誤兩個方面,來與你分享有關裝飾器的一些小知識。

最佳實踐

1. 嘗試用類來實現裝飾器

絕大多數裝飾器都是基於函數和 閉包 實現的,但這並非製造裝飾器的唯一方式。事實上,Python 對某個對象是否能通過裝飾器( @decorator)形式使用只有一個要求:decorator 必須是一個“可被調用(callable)的對象

  1. # 使用 callable 可以檢測某個對象是否“可被調用”

  2. >>> def foo: pass

  3. ...

  4. >>> type(foo)

  5. <class 'function'>

  6. >>> callable(foo)

  7. True

函數自然是“可被調用”的對象。但除了函數外,我們也可以讓任何一個類(class)變得“可被調用”(callable)。辦法很簡單,只要自定義類的 __call__魔法方法即可。

  1. class Foo:

  2. def __call__(self):

  3. print("Hello, __call___")


  4. foo = Foo


  5. # OUTPUT: True

  6. print(callable(foo))

  7. # 調用 foo 實例

  8. # OUTPUT: Hello, __call__

  9. foo

基於這個特性,我們可以很方便的使用類來實現裝飾器。

下面這段代碼,會定義一個名為 @delay(duration)的裝飾器,使用它裝飾過的函數在每次執行前,都會等待額外的duration秒。同時,我們也希望為用戶提供無需等待馬上執行的eager_call接口。

  1. import time

  2. import functools



  3. class DelayFunc:

  4. def __init__(self, duration, func):

  5. self.duration = duration

  6. self.func = func


  7. def __call__(self, *args, **kwargs):

  8. print(f'Wait for {self.duration} seconds...')

  9. time.sleep(self.duration)

  10. return self.func(*args, **kwargs)


  11. def eager_call(self, *args, **kwargs):

  12. print('Call without delay')

  13. return self.func(*args, **kwargs)



  14. def delay(duration):

  15. """裝飾器:推遲某個函數的執行。同時提供 .eager_call 方法立即執行

  16. """

  17. # 此處為了避免定義額外函數,直接使用 functools.partial 幫助構造

  18. # DelayFunc 實例

  19. return functools.partial(DelayFunc, duration)

如何使用裝飾器的樣例代碼:

  1. @delay(duration=2)

  2. def add(a, b):

  3. return a + b



  4. # 這次調用將會延遲 2 秒

  5. add(1, 2)

  6. # 這次調用將會立即執行

  7. add.eager_call(1, 2)

@delay(duration)就是一個基於類來實現的裝飾器。當然,如果你非常熟悉 Python 裡的函數和閉包,上面的delay裝飾器其實也完全可以只用函數來實現。所以,為什麼我們要用類來做這件事呢?

與純函數相比,我覺得使用類實現的裝飾器在特定場景下有幾個優勢:

  • 實現有狀態的裝飾器時,操作類屬性比操作閉包內變量更符合直覺、不易出錯

  • 實現為函數擴充接口的裝飾器時,使用類包裝函數,比直接為函數對象追加屬性更易於維護

  • 更容易實現一個同時兼容裝飾器與上下文管理器協議的對象(參考 unitest.mock.patch)

2. 使用 wrapt 模塊編寫更扁平的裝飾器

在寫裝飾器的過程中,你有沒有碰到過什麼不爽的事情?不管你有沒有,反正我有。我經常在寫代碼的時候,被下面兩件事情搞得特別難受:

  1. 實現帶參數的裝飾器時,層層嵌套的函數代碼特別難寫、難讀

  2. 因為函數和類方法的不同,為前者寫的裝飾器經常沒法直接套用在後者上

比如,在下面的例子裡,我實現了一個生成隨機數並注入為函數參數的裝飾器。

  1. import random



  2. def provide_number(min_num, max_num):

  3. """裝飾器:隨機生成一個在 [min_num, max_num] 範圍的整數,追加為函數的第一個位置參數

  4. """

  5. def wrapper(func):

  6. def decorated(*args, **kwargs):

  7. num = random.randint(min_num, max_num)

  8. # 將 num 作為第一個參數追加後調用函數

  9. return func(num, *args, **kwargs)

  10. return decorated

  11. return wrapper




  12. @provide_number(1, 100)

  13. def print_random_number(num):

  14. print(num)


  15. # 輸出 1-100 的隨機整數

  16. # OUTPUT: 72

  17. print_random_number

@provide_number裝飾器功能看上去很不錯,但它有著我在前面提到的兩個問題:嵌套層級深、無法在類方法上使用。如果直接用它去裝飾類方法,會出現下面的情況:

  1. class Foo:

  2. @provide_number(1, 100)

  3. def print_random_number(self, num):

  4. print(num)


  5. # OUTPUT: <__main__.Foo object at 0x104047278>

  6. Foo.print_random_number

Foo類實例中的print_random_number方法將會輸出類實例self,而不是我們期望的隨機數num

之所以會出現這個結果,是因為類方法(method)和函數(function)二者在工作機制上有著細微不同。如果要修復這個問題, provider_number裝飾器在修改類方法的位置參數時,必須聰明的跳過藏在*args裡面的類實例self變量,才能正確的將num作為第一個參數注入。

這時,就應該是 wrapt 模塊閃亮登場的時候了。wrapt模塊是一個專門幫助你編寫裝飾器的工具庫。利用它,我們可以非常方便的改造provide_number裝飾器,完美解決“嵌套層級深”和“無法通用”兩個問題,

  1. import wrapt


  2. def provide_number(min_num, max_num):

  3. @wrapt.decorator

  4. def wrapper(wrapped, instance, args, kwargs):

  5. # 參數含義:

  6. #

  7. # - wrapped:被裝飾的函數或類方法

  8. # - instance:

  9. # - 如果被裝飾者為普通類方法,該值為類實例

  10. # - 如果被裝飾者為 classmethod 類方法,該值為類

  11. # - 如果被裝飾者為類/函數/靜態方法,該值為 None

  12. #

  13. # - args:調用時的位置參數(注意沒有 * 符號)

  14. # - kwargs:調用時的關鍵字參數(注意沒有 ** 符號)

  15. #

  16. num = random.randint(min_num, max_num)

  17. # 無需關注 wrapped 是類方法或普通函數,直接在頭部追加參數

  18. args = (num,) + args

  19. return wrapped(*args, **kwargs)

  20. return wrapper


  21. <... 應用裝飾器部分代碼省略 ...>


  22. # OUTPUT: 48

  23. Foo.print_random_number

使用 wrapt模塊編寫的裝飾器,相比原來擁有下面這些優勢:

  • 嵌套層級少:使用 @wrapt.decorator可以將兩層嵌套減少為一層

  • 更簡單:處理位置與關鍵字參數時,可以忽略類實例等特殊情況

  • 更靈活:針對 instance值進行條件判斷後,更容易讓裝飾器變得通用

常見錯誤

1. “裝飾器”並不是“裝飾器模式”

“設計模式”是一個在計算機世界裡鼎鼎大名的詞。假如你是一名 Java 程序員,而你一點設計模式都不懂,那麼我打賭你找工作的面試過程一定會度過的相當艱難。

但寫 Python 時,我們極少談起“設計模式”。雖然 Python 也是一門支持面向對象的編程語言,但它的 鴨子類型 設計以及出色的動態特性決定了,大部分設計模式對我們來說並不是必需品。所以,很多 Python 程序員在工作很長一段時間後,可能並沒有真正應用過幾種設計模式。

不過 “裝飾器模式(Decorator Pattern)” 是個例外。因為 Python 的“裝飾器”和“裝飾器模式”有著一模一樣的名字,我不止一次聽到有人把它們倆當成一回事,認為使用“裝飾器”就是在實踐“裝飾器模式”。但事實上,它們是兩個完全不同的東西。

“裝飾器模式”是一個完全基於“面向對象”衍生出的編程手法。它擁有幾個關鍵組成:一個統一的接口定義若干個遵循該接口的類類與類之間一層一層的包裝。最終由它們共同形成一種“裝飾”的效果。

而 Python 裡的“裝飾器”和“面向對象”沒有任何直接聯繫,它完全可以只是發生在函數和函數間的把戲。事實上,“裝飾器”並沒有提供某種無法替代的功能,它僅僅就是一顆“語法糖”而已。下面這段使用了裝飾器的代碼:

  1. @log_time

  2. @cache_result

  3. def foo: pass

基本完全等同於下面這樣:

  1. def foo: pass


  2. foo = log_time(cache_result(foo))

裝飾器最大的功勞,在於讓我們在某些特定場景時,可以寫出更符合直覺、易於閱讀的代碼。它只是一顆“糖”,並不是某個面向對象領域的複雜編程模式。

Hint: 在 Python 官網上有一個 實現了裝飾器模式的例子,你可以讀讀這個例子來更好的瞭解它。

2. 記得用 functools.wraps 裝飾內層函數

下面是一個簡單的裝飾器,專門用來打印函數調用耗時:

  1. import time



  2. def timer(wrapped):

  3. """裝飾器:記錄並打印函數耗時"""

  4. def decorated(*args, **kwargs):

  5. st = time.time

  6. ret = wrapped(*args, **kwargs)

  7. print('execution take: {} seconds'.format(time.time - st))

  8. return ret

  9. return decorated



  10. @timer

  11. def random_sleep:

  12. """隨機睡眠一小會"""

  13. time.sleep(random.random)

timer裝飾器雖然沒有錯誤,但是使用它裝飾函數後,函數的原始簽名就會被破壞。也就是說你再也沒辦法正確拿到random_sleep函數的名稱、文檔內容了,所有簽名都會變成內層函數decorated的值:

  1. print(random_sleep.__name__)

  2. # 輸出 'decorated'

  3. print(random_sleep.__doc__)

  4. # 輸出 None

這雖然只是個小問題,但在某些時候也可能會導致難以察覺的 bug。幸運的是,標準庫 functools為它提供瞭解決方案,你只需要在定義裝飾器時,用另外一個裝飾器再裝飾一下內層decorated函數就行。

聽上去有點繞,但其實就是新增一行代碼而已:

  1. def timer(wrapped):

  2. # 將 wrapper 函數的真實簽名賦值到 decorated 上

  3. @functools.wraps(wrapped)

  4. def decorated(*args, **kwargs):

  5. # <...> 已省略

  6. return decorated

這樣處理後, timer裝飾器就不會影響它所裝飾的函數了。

  1. print(random_sleep.__name__)

  2. # 輸出 'random_sleep'

  3. print(random_sleep.__doc__)

  4. # 輸出 '隨機睡眠一小會'

3. 修改外層變量時記得使用 nonlocal

裝飾器是對函數對象的一個高級應用。在編寫裝飾器的過程中,你會經常碰到內層函數需要修改外層函數變量的情況。就像下面這個裝飾器一樣:

  1. import functools


  2. def counter(func):

  3. """裝飾器:記錄並打印調用次數"""

  4. count = 0

  5. @functools.wraps(func)

  6. def decorated(*args, **kwargs):

  7. # 次數累加

  8. count += 1

  9. print(f"Count: {count}")

  10. return func(*args, **kwargs)

  11. return decorated


  12. @counter

  13. def foo:

  14. pass


  15. foo

為了統計函數調用次數,我們需要在 decorated函數內部修改外層函數定義的count變量的值。但是,上面這段代碼是有問題的,在執行它時解釋器會報錯:

  1. Traceback (most recent call last):

  2. File "counter.py", line 22, in <module>

  3. foo

  4. File "counter.py", line 11, in decorated

  5. count += 1

  6. UnboundLocalError: local variable 'count' referenced before assignment

這個錯誤是由 counterdecorated函數互相嵌套的作用域引起的。

當解釋器執行到 count+=1時,並不知道count是一個在外層作用域定義的變量,它把count當做一個局部變量,並在當前作用域內查找。最終卻沒有找到有關count變量的任何定義,然後拋出錯誤。

為了解決這個問題,我們需要通過 nonlocal關鍵字告訴解釋器:“count 變量並不屬於當前的 local 作用域,去外面找找吧”,之前的錯誤就可以得到解決。

  1. def decorated(*args, **kwargs):

  2. nonlocal count

  3. count += 1

  4. # <... 已省略 ...>

Hint:如果要了解更多有關 nonlocal 關鍵字的歷史,可以查閱 PEP-3104

總結

在這篇文章裡,我與你分享了有關裝飾器的一些技巧與小知識。

一些要點總結:

  • 一切 callable 的對象都可以被用來實現裝飾器

  • 混合使用函數與類,可以更好的實現裝飾器

  • wrapt 模塊很有用,用它可以幫助我們用更簡單的代碼寫出複雜裝飾器

  • “裝飾器”只是語法糖,它不是“裝飾器模式”

  • 裝飾器會改變函數的原始簽名,你需要 functools.wraps

  • 在內層函數修改外層函數的變量時,需要使用 nonlocal關鍵字

看完文章的你,有沒有什麼想吐槽的?請留言或者在 項目 Github Issues 告訴我吧。

回覆下方「關鍵詞」,獲取優質資源

回覆關鍵詞「 pybook03」,立即獲取主頁君與小夥伴一起翻譯的《Think Python 2e》電子版

回覆關鍵詞「pybooks02」,立即獲取 O'Reilly 出版社推出的免費 Python 相關電子書合集

回覆關鍵詞「書單02」,立即獲取主頁君整理的 10 本 Python 入門書的電子版

"不懂裝飾器,就不是真正會 Python

文 | piglei@piglei 公眾號

編輯 | EarlGrey

推薦 | 編程派公眾號

裝飾器(Decorator) 是 Python 裡的一種特殊工具,它為我們提供了一種在函數外部修改函數的靈活能力。它有點像一頂畫著獨一無二 @符號的神奇帽子,只要將它戴在函數頭頂上,就能悄無聲息的改變函數本身的行為。

你可能已經和裝飾器打過不少交道了。在做面向對象編程時,我們就經常會用到 @staticmethod@classmethod兩個內置裝飾器。此外,如果你接觸過 click 模塊,就更不會對裝飾器感到陌生。click 最為人所稱道的參數定義接口@click.option(...)就是利用裝飾器實現的。

除了用裝飾器,我們也經常需要自己寫一些裝飾器。在這篇文章裡,我將從 最佳實踐常見錯誤兩個方面,來與你分享有關裝飾器的一些小知識。

最佳實踐

1. 嘗試用類來實現裝飾器

絕大多數裝飾器都是基於函數和 閉包 實現的,但這並非製造裝飾器的唯一方式。事實上,Python 對某個對象是否能通過裝飾器( @decorator)形式使用只有一個要求:decorator 必須是一個“可被調用(callable)的對象

  1. # 使用 callable 可以檢測某個對象是否“可被調用”

  2. >>> def foo: pass

  3. ...

  4. >>> type(foo)

  5. <class 'function'>

  6. >>> callable(foo)

  7. True

函數自然是“可被調用”的對象。但除了函數外,我們也可以讓任何一個類(class)變得“可被調用”(callable)。辦法很簡單,只要自定義類的 __call__魔法方法即可。

  1. class Foo:

  2. def __call__(self):

  3. print("Hello, __call___")


  4. foo = Foo


  5. # OUTPUT: True

  6. print(callable(foo))

  7. # 調用 foo 實例

  8. # OUTPUT: Hello, __call__

  9. foo

基於這個特性,我們可以很方便的使用類來實現裝飾器。

下面這段代碼,會定義一個名為 @delay(duration)的裝飾器,使用它裝飾過的函數在每次執行前,都會等待額外的duration秒。同時,我們也希望為用戶提供無需等待馬上執行的eager_call接口。

  1. import time

  2. import functools



  3. class DelayFunc:

  4. def __init__(self, duration, func):

  5. self.duration = duration

  6. self.func = func


  7. def __call__(self, *args, **kwargs):

  8. print(f'Wait for {self.duration} seconds...')

  9. time.sleep(self.duration)

  10. return self.func(*args, **kwargs)


  11. def eager_call(self, *args, **kwargs):

  12. print('Call without delay')

  13. return self.func(*args, **kwargs)



  14. def delay(duration):

  15. """裝飾器:推遲某個函數的執行。同時提供 .eager_call 方法立即執行

  16. """

  17. # 此處為了避免定義額外函數,直接使用 functools.partial 幫助構造

  18. # DelayFunc 實例

  19. return functools.partial(DelayFunc, duration)

如何使用裝飾器的樣例代碼:

  1. @delay(duration=2)

  2. def add(a, b):

  3. return a + b



  4. # 這次調用將會延遲 2 秒

  5. add(1, 2)

  6. # 這次調用將會立即執行

  7. add.eager_call(1, 2)

@delay(duration)就是一個基於類來實現的裝飾器。當然,如果你非常熟悉 Python 裡的函數和閉包,上面的delay裝飾器其實也完全可以只用函數來實現。所以,為什麼我們要用類來做這件事呢?

與純函數相比,我覺得使用類實現的裝飾器在特定場景下有幾個優勢:

  • 實現有狀態的裝飾器時,操作類屬性比操作閉包內變量更符合直覺、不易出錯

  • 實現為函數擴充接口的裝飾器時,使用類包裝函數,比直接為函數對象追加屬性更易於維護

  • 更容易實現一個同時兼容裝飾器與上下文管理器協議的對象(參考 unitest.mock.patch)

2. 使用 wrapt 模塊編寫更扁平的裝飾器

在寫裝飾器的過程中,你有沒有碰到過什麼不爽的事情?不管你有沒有,反正我有。我經常在寫代碼的時候,被下面兩件事情搞得特別難受:

  1. 實現帶參數的裝飾器時,層層嵌套的函數代碼特別難寫、難讀

  2. 因為函數和類方法的不同,為前者寫的裝飾器經常沒法直接套用在後者上

比如,在下面的例子裡,我實現了一個生成隨機數並注入為函數參數的裝飾器。

  1. import random



  2. def provide_number(min_num, max_num):

  3. """裝飾器:隨機生成一個在 [min_num, max_num] 範圍的整數,追加為函數的第一個位置參數

  4. """

  5. def wrapper(func):

  6. def decorated(*args, **kwargs):

  7. num = random.randint(min_num, max_num)

  8. # 將 num 作為第一個參數追加後調用函數

  9. return func(num, *args, **kwargs)

  10. return decorated

  11. return wrapper




  12. @provide_number(1, 100)

  13. def print_random_number(num):

  14. print(num)


  15. # 輸出 1-100 的隨機整數

  16. # OUTPUT: 72

  17. print_random_number

@provide_number裝飾器功能看上去很不錯,但它有著我在前面提到的兩個問題:嵌套層級深、無法在類方法上使用。如果直接用它去裝飾類方法,會出現下面的情況:

  1. class Foo:

  2. @provide_number(1, 100)

  3. def print_random_number(self, num):

  4. print(num)


  5. # OUTPUT: <__main__.Foo object at 0x104047278>

  6. Foo.print_random_number

Foo類實例中的print_random_number方法將會輸出類實例self,而不是我們期望的隨機數num

之所以會出現這個結果,是因為類方法(method)和函數(function)二者在工作機制上有著細微不同。如果要修復這個問題, provider_number裝飾器在修改類方法的位置參數時,必須聰明的跳過藏在*args裡面的類實例self變量,才能正確的將num作為第一個參數注入。

這時,就應該是 wrapt 模塊閃亮登場的時候了。wrapt模塊是一個專門幫助你編寫裝飾器的工具庫。利用它,我們可以非常方便的改造provide_number裝飾器,完美解決“嵌套層級深”和“無法通用”兩個問題,

  1. import wrapt


  2. def provide_number(min_num, max_num):

  3. @wrapt.decorator

  4. def wrapper(wrapped, instance, args, kwargs):

  5. # 參數含義:

  6. #

  7. # - wrapped:被裝飾的函數或類方法

  8. # - instance:

  9. # - 如果被裝飾者為普通類方法,該值為類實例

  10. # - 如果被裝飾者為 classmethod 類方法,該值為類

  11. # - 如果被裝飾者為類/函數/靜態方法,該值為 None

  12. #

  13. # - args:調用時的位置參數(注意沒有 * 符號)

  14. # - kwargs:調用時的關鍵字參數(注意沒有 ** 符號)

  15. #

  16. num = random.randint(min_num, max_num)

  17. # 無需關注 wrapped 是類方法或普通函數,直接在頭部追加參數

  18. args = (num,) + args

  19. return wrapped(*args, **kwargs)

  20. return wrapper


  21. <... 應用裝飾器部分代碼省略 ...>


  22. # OUTPUT: 48

  23. Foo.print_random_number

使用 wrapt模塊編寫的裝飾器,相比原來擁有下面這些優勢:

  • 嵌套層級少:使用 @wrapt.decorator可以將兩層嵌套減少為一層

  • 更簡單:處理位置與關鍵字參數時,可以忽略類實例等特殊情況

  • 更靈活:針對 instance值進行條件判斷後,更容易讓裝飾器變得通用

常見錯誤

1. “裝飾器”並不是“裝飾器模式”

“設計模式”是一個在計算機世界裡鼎鼎大名的詞。假如你是一名 Java 程序員,而你一點設計模式都不懂,那麼我打賭你找工作的面試過程一定會度過的相當艱難。

但寫 Python 時,我們極少談起“設計模式”。雖然 Python 也是一門支持面向對象的編程語言,但它的 鴨子類型 設計以及出色的動態特性決定了,大部分設計模式對我們來說並不是必需品。所以,很多 Python 程序員在工作很長一段時間後,可能並沒有真正應用過幾種設計模式。

不過 “裝飾器模式(Decorator Pattern)” 是個例外。因為 Python 的“裝飾器”和“裝飾器模式”有著一模一樣的名字,我不止一次聽到有人把它們倆當成一回事,認為使用“裝飾器”就是在實踐“裝飾器模式”。但事實上,它們是兩個完全不同的東西。

“裝飾器模式”是一個完全基於“面向對象”衍生出的編程手法。它擁有幾個關鍵組成:一個統一的接口定義若干個遵循該接口的類類與類之間一層一層的包裝。最終由它們共同形成一種“裝飾”的效果。

而 Python 裡的“裝飾器”和“面向對象”沒有任何直接聯繫,它完全可以只是發生在函數和函數間的把戲。事實上,“裝飾器”並沒有提供某種無法替代的功能,它僅僅就是一顆“語法糖”而已。下面這段使用了裝飾器的代碼:

  1. @log_time

  2. @cache_result

  3. def foo: pass

基本完全等同於下面這樣:

  1. def foo: pass


  2. foo = log_time(cache_result(foo))

裝飾器最大的功勞,在於讓我們在某些特定場景時,可以寫出更符合直覺、易於閱讀的代碼。它只是一顆“糖”,並不是某個面向對象領域的複雜編程模式。

Hint: 在 Python 官網上有一個 實現了裝飾器模式的例子,你可以讀讀這個例子來更好的瞭解它。

2. 記得用 functools.wraps 裝飾內層函數

下面是一個簡單的裝飾器,專門用來打印函數調用耗時:

  1. import time



  2. def timer(wrapped):

  3. """裝飾器:記錄並打印函數耗時"""

  4. def decorated(*args, **kwargs):

  5. st = time.time

  6. ret = wrapped(*args, **kwargs)

  7. print('execution take: {} seconds'.format(time.time - st))

  8. return ret

  9. return decorated



  10. @timer

  11. def random_sleep:

  12. """隨機睡眠一小會"""

  13. time.sleep(random.random)

timer裝飾器雖然沒有錯誤,但是使用它裝飾函數後,函數的原始簽名就會被破壞。也就是說你再也沒辦法正確拿到random_sleep函數的名稱、文檔內容了,所有簽名都會變成內層函數decorated的值:

  1. print(random_sleep.__name__)

  2. # 輸出 'decorated'

  3. print(random_sleep.__doc__)

  4. # 輸出 None

這雖然只是個小問題,但在某些時候也可能會導致難以察覺的 bug。幸運的是,標準庫 functools為它提供瞭解決方案,你只需要在定義裝飾器時,用另外一個裝飾器再裝飾一下內層decorated函數就行。

聽上去有點繞,但其實就是新增一行代碼而已:

  1. def timer(wrapped):

  2. # 將 wrapper 函數的真實簽名賦值到 decorated 上

  3. @functools.wraps(wrapped)

  4. def decorated(*args, **kwargs):

  5. # <...> 已省略

  6. return decorated

這樣處理後, timer裝飾器就不會影響它所裝飾的函數了。

  1. print(random_sleep.__name__)

  2. # 輸出 'random_sleep'

  3. print(random_sleep.__doc__)

  4. # 輸出 '隨機睡眠一小會'

3. 修改外層變量時記得使用 nonlocal

裝飾器是對函數對象的一個高級應用。在編寫裝飾器的過程中,你會經常碰到內層函數需要修改外層函數變量的情況。就像下面這個裝飾器一樣:

  1. import functools


  2. def counter(func):

  3. """裝飾器:記錄並打印調用次數"""

  4. count = 0

  5. @functools.wraps(func)

  6. def decorated(*args, **kwargs):

  7. # 次數累加

  8. count += 1

  9. print(f"Count: {count}")

  10. return func(*args, **kwargs)

  11. return decorated


  12. @counter

  13. def foo:

  14. pass


  15. foo

為了統計函數調用次數,我們需要在 decorated函數內部修改外層函數定義的count變量的值。但是,上面這段代碼是有問題的,在執行它時解釋器會報錯:

  1. Traceback (most recent call last):

  2. File "counter.py", line 22, in <module>

  3. foo

  4. File "counter.py", line 11, in decorated

  5. count += 1

  6. UnboundLocalError: local variable 'count' referenced before assignment

這個錯誤是由 counterdecorated函數互相嵌套的作用域引起的。

當解釋器執行到 count+=1時,並不知道count是一個在外層作用域定義的變量,它把count當做一個局部變量,並在當前作用域內查找。最終卻沒有找到有關count變量的任何定義,然後拋出錯誤。

為了解決這個問題,我們需要通過 nonlocal關鍵字告訴解釋器:“count 變量並不屬於當前的 local 作用域,去外面找找吧”,之前的錯誤就可以得到解決。

  1. def decorated(*args, **kwargs):

  2. nonlocal count

  3. count += 1

  4. # <... 已省略 ...>

Hint:如果要了解更多有關 nonlocal 關鍵字的歷史,可以查閱 PEP-3104

總結

在這篇文章裡,我與你分享了有關裝飾器的一些技巧與小知識。

一些要點總結:

  • 一切 callable 的對象都可以被用來實現裝飾器

  • 混合使用函數與類,可以更好的實現裝飾器

  • wrapt 模塊很有用,用它可以幫助我們用更簡單的代碼寫出複雜裝飾器

  • “裝飾器”只是語法糖,它不是“裝飾器模式”

  • 裝飾器會改變函數的原始簽名,你需要 functools.wraps

  • 在內層函數修改外層函數的變量時,需要使用 nonlocal關鍵字

看完文章的你,有沒有什麼想吐槽的?請留言或者在 項目 Github Issues 告訴我吧。

回覆下方「關鍵詞」,獲取優質資源

回覆關鍵詞「 pybook03」,立即獲取主頁君與小夥伴一起翻譯的《Think Python 2e》電子版

回覆關鍵詞「pybooks02」,立即獲取 O'Reilly 出版社推出的免費 Python 相關電子書合集

回覆關鍵詞「書單02」,立即獲取主頁君整理的 10 本 Python 入門書的電子版

不懂裝飾器,就不是真正會 Python

你想要的 IT 電子資源,這裡可能都有

Python 或將超越 C、Java,成為最受歡迎的語言

Python 容器使用的 5 個技巧和 2 個誤區

如何寫出優雅的 Python 函數?

題圖:pexels,CC0 授權。

"

相關推薦

推薦中...