<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Python 工匠:使用裝飾器的技巧

          共 6848字,需瀏覽 14分鐘

           ·

          2019-07-30 08:15

          Python 裝飾器可謂是非常非常好用的,但其實它用起來還是有不少講究的,這里分享給大家一片文章,介紹一些使用裝飾器的技巧。

          Python 裝飾器可謂是非常非常好用的,但其實它用起來還是有不少講究的,這里分享給大家一片文章,介紹一些使用裝飾器的技巧。

          轉(zhuǎn)載來源

          公眾號:piglei

          閱讀本文大概需要 8?分鐘。


          前言

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

          你可能已經(jīng)和裝飾器打過不少交道了。在做面向?qū)ο缶幊虝r,我們就經(jīng)常會用到 @staticmethod@classmethod 兩個內(nèi)置裝飾器。此外,如果你接觸過 click 模塊,就更不會對裝飾器感到陌生。click 最為人所稱道的參數(shù)定義接口 @click.option(...) 就是利用裝飾器實現(xiàn)的。

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

          最佳實踐

          1. 嘗試用類來實現(xiàn)裝飾器

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

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

          2. >>>def foo():pass

          3. ...

          4. >>> type(foo)

          5. <class'function'>

          6. >>> callable(foo)

          7. True

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

          1. classFoo:

          2. def __call__(self):

          3. print("Hello, __call___")

          4. foo =Foo()

          5. # OUTPUT: True

          6. print(callable(foo))

          7. # 調(diào)用 foo 實例

          8. # OUTPUT: Hello, __call__

          9. foo()

          基于這個特性,我們可以很方便的使用類來實現(xiàn)裝飾器。

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

          1. import time

          2. import functools

          3. classDelayFunc:

          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. """裝飾器:推遲某個函數(shù)的執(zhí)行。同時提供 .eager_call 方法立即執(zhí)行

          16. """

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

          18. # DelayFunc 實例

          19. return functools.partial(DelayFunc, duration)

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

          1. @delay(duration=2)

          2. def add(a, b):

          3. return a + b

          4. # 這次調(diào)用將會延遲 2 秒

          5. add(1,2)

          6. # 這次調(diào)用將會立即執(zhí)行

          7. add.eager_call(1,2)

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

          與純函數(shù)相比,我覺得使用類實現(xiàn)的裝飾器在特定場景下有幾個優(yōu)勢:

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

          • 實現(xiàn)為函數(shù)擴充接口的裝飾器時,使用類包裝函數(shù),比直接為函數(shù)對象追加屬性更易于維護

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

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

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

          1. 實現(xiàn)帶參數(shù)的裝飾器時,層層嵌套的函數(shù)代碼特別難寫、難讀

          2. 因為函數(shù)和類方法的不同,為前者寫的裝飾器經(jīng)常沒法直接套用在后者上

          比如,在下面的例子里,我實現(xiàn)了一個生成隨機數(shù)并注入為函數(shù)參數(shù)的裝飾器。

          1. import random

          2. def provide_number(min_num, max_num):

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

          4. """

          5. def wrapper(func):

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

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

          8. # 將 num 作為第一個參數(shù)追加后調(diào)用函數(shù)

          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 的隨機整數(shù)

          16. # OUTPUT: 72

          17. print_random_number()

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

          1. classFoo:

          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 ,而不是我們期望的隨機數(shù) num

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

          這時,就應該是 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. # 參數(shù)含義:

          6. #

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

          8. # - instance:

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

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

          11. # - 如果被裝飾者為類/函數(shù)/靜態(tài)方法,該值為 None

          12. #

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

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

          15. #

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

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

          18. args =(num,)+ args

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

          20. return wrapper

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

          22. # OUTPUT: 48

          23. Foo().print_random_number()

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

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

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

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

          常見錯誤

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

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

          但寫 Python 時,我們極少談起“設計模式”。雖然 Python 也是一門支持面向?qū)ο蟮木幊陶Z言,但它的 鴨子類型 設計以及出色的動態(tài)特性決定了,大部分設計模式對我們來說并不是必需品。所以,很多 Python 程序員在工作很長一段時間后,可能并沒有真正應用過幾種設計模式。

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

          “裝飾器模式”是一個完全基于“面向?qū)ο蟆毖苌龅木幊淌址āK鼡碛袔讉€關鍵組成:一個統(tǒng)一的接口定義若干個遵循該接口的類類與類之間一層一層的包裝。最終由它們共同形成一種“裝飾”的效果。

          而 Python 里的“裝飾器”和“面向?qū)ο蟆睕]有任何直接聯(lián)系,它完全可以只是發(fā)生在函數(shù)和函數(shù)間的把戲。事實上,“裝飾器”并沒有提供某種無法替代的功能,它僅僅就是一顆“語法糖”而已。下面這段使用了裝飾器的代碼:

          1. @log_time

          2. @cache_result

          3. def foo():pass

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

          1. def foo():pass

          2. foo = log_time(cache_result(foo))

          裝飾器最大的功勞,在于讓我們在某些特定場景時,可以寫出更符合直覺、易于閱讀的代碼。它只是一顆“糖”,并不是某個面向?qū)ο箢I域的復雜編程模式。

          Hint: 在 Python 官網(wǎng)上有一個 實現(xiàn)了裝飾器模式的例子,你可以讀讀這個例子來更好的了解它。

          2. 記得用 functools.wraps() 裝飾內(nèi)層函數(shù)

          下面是一個簡單的裝飾器,專門用來打印函數(shù)調(diào)用耗時:

          1. import time

          2. def timer(wrapped):

          3. """裝飾器:記錄并打印函數(shù)耗時"""

          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 裝飾器雖然沒有錯誤,但是使用它裝飾函數(shù)后,函數(shù)的原始簽名就會被破壞。也就是說你再也沒辦法正確拿到 random_sleep 函數(shù)的名稱、文檔內(nèi)容了,所有簽名都會變成內(nèi)層函數(shù) decorated 的值:

          1. print(random_sleep.__name__)

          2. # 輸出 'decorated'

          3. print(random_sleep.__doc__)

          4. # 輸出 None

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

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

          1. def timer(wrapped):

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

          3. @functools.wraps(wrapped)

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

          5. # <...> 已省略

          6. return decorated

          這樣處理后, timer 裝飾器就不會影響它所裝飾的函數(shù)了。

          1. print(random_sleep.__name__)

          2. # 輸出 'random_sleep'

          3. print(random_sleep.__doc__)

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

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

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

          1. import functools

          2. def counter(func):

          3. """裝飾器:記錄并打印調(diào)用次數(shù)"""

          4. count =0

          5. @functools.wraps(func)

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

          7. # 次數(shù)累加

          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()

          為了統(tǒng)計函數(shù)調(diào)用次數(shù),我們需要在 decorated 函數(shù)內(nèi)部修改外層函數(shù)定義的 count 變量的值。但是,上面這段代碼是有問題的,在執(zhí)行它時解釋器會報錯:

          1. Traceback(most recent call last):

          2. File"counter.py", line 22,in

          3. foo()

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

          5. count +=1

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

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

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

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

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

          2. nonlocal count

          3. count +=1

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

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

          總結

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

          一些要點總結:

          • 一切 callable 的對象都可以被用來實現(xiàn)裝飾器

          • 混合使用函數(shù)與類,可以更好的實現(xiàn)裝飾器

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

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

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

          • 在內(nèi)層函數(shù)修改外層函數(shù)的變量時,需要使用?nonlocal?關鍵字

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

          附錄

          • 題圖來源: Photo by Clem Onojeghuo on Unsplash

          • 更多系列文章地址:https://github.com/piglei/one-python-craftsman

          推薦閱讀

          1

          跟繁瑣的命令行說拜拜!Gerapy分布式爬蟲管理框架來襲!

          2

          跟繁瑣的模型說拜拜!深度學習腳手架 ModelZoo 來襲!

          3

          只會用Selenium爬網(wǎng)頁?Appium爬App了解一下

          4??

          媽媽再也不用擔心爬蟲被封號了!手把手教你搭建Cookies池

          崔慶才

          靜覓博客博主,《Python3網(wǎng)絡爬蟲開發(fā)實戰(zhàn)》作者

          隱形字

          個人公眾號:進擊的Coder

          74b82c8a16291ad0937e0e6565839cba.webp835ab9e786b4ca648bb97537cb54f276.webp

          長按識別二維碼關注


          好文和朋友一起看~
          瀏覽 76
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  高清在线视频一区 | 91最新在线 | 免费在线观看亚洲视频 | 成人无码不卡免费视频 | 操逼网站在线 |