真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

Python中使用裝飾器的各種技巧

這篇文章將為大家詳細(xì)講解有關(guān)Python中使用裝飾器的各種技巧,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。

創(chuàng)新互聯(lián)-成都網(wǎng)站建設(shè)公司,專(zhuān)注成都網(wǎng)站建設(shè)、網(wǎng)站設(shè)計(jì)、網(wǎng)站營(yíng)銷(xiāo)推廣,域名與空間,雅安服務(wù)器托管,網(wǎng)站托管運(yùn)營(yíng)有關(guān)企業(yè)網(wǎng)站制作方案、改版、費(fèi)用等問(wèn)題,請(qǐng)聯(lián)系創(chuàng)新互聯(lián)。

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

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

除了用裝飾器,我們也經(jīng)常需要自己寫(xiě)一些裝飾器。在這篇文章里,我將從 最佳實(shí)踐 和 常見(jiàn)錯(cuò)誤 兩個(gè)方面,來(lái)與你分享有關(guān)裝飾器的一些小知識(shí)。

1. 嘗試用類(lèi)來(lái)實(shí)現(xiàn)裝飾器

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

# 使用 callable 可以檢測(cè)某個(gè)對(duì)象是否“可被調(diào)用”
>>> def foo(): pass
...
>>> type(foo)

>>> callable(foo)
True

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

class Foo:
    def __call__(self):
        print("Hello, __call___")
foo = Foo()
# OUTPUT: True
print(callable(foo))
# 調(diào)用 foo 實(shí)例
# OUTPUT: Hello, __call__
foo()

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

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

import time
import functools
class DelayFunc:
    def __init__(self,  duration, func):
        self.duration = duration
        self.func = func
def __call__(self, *args, **kwargs):
        print(f'Wait for {self.duration} seconds...')
        time.sleep(self.duration)
        return self.func(*args, **kwargs)
def eager_call(self, *args, **kwargs):
        print('Call without delay')
        return self.func(*args, **kwargs)
def delay(duration):
    """裝飾器:推遲某個(gè)函數(shù)的執(zhí)行。同時(shí)提供 .eager_call 方法立即執(zhí)行
    """
    # 此處為了避免定義額外函數(shù),直接使用 functools.partial 幫助構(gòu)造
    # DelayFunc 實(shí)例
    return functools.partial(DelayFunc, duration)
如何使用裝飾器的樣例代碼:
@delay(duration=2)
def add(a, b):
    return a + b
# 這次調(diào)用將會(huì)延遲 2 秒
add(1, 2)
# 這次調(diào)用將會(huì)立即執(zhí)行
add.eager_call(1, 2)

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

與純函數(shù)相比,我覺(jué)得使用類(lèi)實(shí)現(xiàn)的裝飾器在特定場(chǎng)景下有幾個(gè)優(yōu)勢(shì):

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

? 實(shí)現(xiàn)為函數(shù)擴(kuò)充接口的裝飾器時(shí),使用類(lèi)包裝函數(shù),比直接為函數(shù)對(duì)象追加屬性更易于維護(hù)

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

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

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

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

2. 因?yàn)楹瘮?shù)和類(lèi)方法的不同,為前者寫(xiě)的裝飾器經(jīng)常沒(méi)法直接套用在后者上

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

import random
def provide_number(min_num, max_num):
    """裝飾器:隨機(jī)生成一個(gè)在 [min_num, max_num] 范圍的整數(shù),追加為函數(shù)的第一個(gè)位置參數(shù)
    """
    def wrapper(func):
        def decorated(*args, **kwargs):
            num = random.randint(min_num, max_num)
            # 將 num 作為第一個(gè)參數(shù)追加后調(diào)用函數(shù)
            return func(num, *args, **kwargs)
        return decorated
    return wrapper
@provide_number(1, 100)
def print_random_number(num):
    print(num)
# 輸出 1-100 的隨機(jī)整數(shù)
# OUTPUT: 72
print_random_number()
@provide_number 裝飾器功能看上去很不錯(cuò),但它有著我在前面提到的兩個(gè)問(wèn)題:嵌套層級(jí)深、無(wú)法在類(lèi)方法上使用。如果直接用它去裝飾類(lèi)方法,會(huì)出現(xiàn)下面的情況:
class Foo:
    @provide_number(1, 100)
    def print_random_number(self, num):
        print(num)
# OUTPUT: <__main__.Foo object at 0x104047278>
Foo().print_random_number()

Foo 類(lèi)實(shí)例中的 print_random_number 方法將會(huì)輸出類(lèi)實(shí)例 self ,而不是我們期望的隨機(jī)數(shù) num。

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

這時(shí),就應(yīng)該是 wrapt 模塊閃亮登場(chǎng)的時(shí)候了。wrapt 模塊是一個(gè)專(zhuān)門(mén)幫助你編寫(xiě)裝飾器的工具庫(kù)。利用它,我們可以非常方便的改造 provide_number 裝飾器,完美解決“嵌套層級(jí)深”和“無(wú)法通用”兩個(gè)問(wèn)題,

import wrapt
def provide_number(min_num, max_num):
    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        # 參數(shù)含義:
        #
        # - wrapped:被裝飾的函數(shù)或類(lèi)方法
        # - instance:
        #   - 如果被裝飾者為普通類(lèi)方法,該值為類(lèi)實(shí)例
        #   - 如果被裝飾者為 classmethod 類(lèi)方法,該值為類(lèi)
        #   - 如果被裝飾者為類(lèi)/函數(shù)/靜態(tài)方法,該值為 None
        #
        # - args:調(diào)用時(shí)的位置參數(shù)(注意沒(méi)有 * 符號(hào))
        # - kwargs:調(diào)用時(shí)的關(guān)鍵字參數(shù)(注意沒(méi)有 ** 符號(hào))
        #
        num = random.randint(min_num, max_num)
        # 無(wú)需關(guān)注 wrapped 是類(lèi)方法或普通函數(shù),直接在頭部追加參數(shù)
        args = (num,) + args
        return wrapped(*args, **kwargs)
    return wrapper
<... 應(yīng)用裝飾器部分代碼省略 ...>
# OUTPUT: 48
Foo().print_random_number()

使用 wrapt 模塊編寫(xiě)的裝飾器,相比原來(lái)?yè)碛邢旅孢@些優(yōu)勢(shì):

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

? 更簡(jiǎn)單:處理位置與關(guān)鍵字參數(shù)時(shí),可以忽略類(lèi)實(shí)例等特殊情況

? 更靈活:針對(duì) instance 值進(jìn)行條件判斷后,更容易讓裝飾器變得通用

常見(jiàn)錯(cuò)誤

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

“設(shè)計(jì)模式”是一個(gè)在計(jì)算機(jī)世界里鼎鼎大名的詞。假如你是一名 Java 程序員,而你一點(diǎn)設(shè)計(jì)模式都不懂,那么我打賭你找工作的面試過(guò)程一定會(huì)度過(guò)的相當(dāng)艱難。

但寫(xiě) Python 時(shí),我們極少談起“設(shè)計(jì)模式”。雖然 Python 也是一門(mén)支持面向?qū)ο蟮木幊陶Z(yǔ)言,但它的 鴨子類(lèi)型設(shè)計(jì)以及出色的動(dòng)態(tài)特性決定了,大部分設(shè)計(jì)模式對(duì)我們來(lái)說(shuō)并不是必需品。所以,很多 Python 程序員在工作很長(zhǎng)一段時(shí)間后,可能并沒(méi)有真正應(yīng)用過(guò)幾種設(shè)計(jì)模式。

不過(guò) “裝飾器模式(Decorator Pattern)” 是個(gè)例外。因?yàn)?Python 的“裝飾器”和“裝飾器模式”有著一模一樣的名字,我不止一次聽(tīng)到有人把它們倆當(dāng)成一回事,認(rèn)為使用“裝飾器”就是在實(shí)踐“裝飾器模式”。但事實(shí)上,它們是兩個(gè)完全不同的東西。

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

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

@log_time
@cache_result
def foo(): pass

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

def foo(): pass
foo = log_time(cache_result(foo))

裝飾器最大的功勞,在于讓我們?cè)谀承┨囟▓?chǎng)景時(shí),可以寫(xiě)出更符合直覺(jué)、易于閱讀的代碼。它只是一顆“糖”,并不是某個(gè)面向?qū)ο箢I(lǐng)域的復(fù)雜編程模式。

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

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

下面是一個(gè)簡(jiǎn)單的裝飾器,專(zhuān)門(mén)用來(lái)打印函數(shù)調(diào)用耗時(shí):

import time
def timer(wrapped):
    """裝飾器:記錄并打印函數(shù)耗時(shí)"""
    def decorated(*args, **kwargs):
        st = time.time()
        ret = wrapped(*args, **kwargs)
        print('execution take: {} seconds'.format(time.time() - st))
        return ret
    return decorated
@timer
def random_sleep():
    """隨機(jī)睡眠一小會(huì)"""
    time.sleep(random.random())

timer 裝飾器雖然沒(méi)有錯(cuò)誤,但是使用它裝飾函數(shù)后,函數(shù)的原始簽名就會(huì)被破壞。也就是說(shuō)你再也沒(méi)辦法正確拿到 random_sleep 函數(shù)的名稱(chēng)、文檔內(nèi)容了,所有簽名都會(huì)變成內(nèi)層函數(shù) decorated 的值:

print(random_sleep.__name__)
# 輸出 'decorated'
print(random_sleep.__doc__)
# 輸出 None

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

聽(tīng)上去有點(diǎn)繞,但其實(shí)就是新增一行代碼而已:

def timer(wrapped):
    # 將 wrapper 函數(shù)的真實(shí)簽名賦值到 decorated 上
    @functools.wraps(wrapped)
    def decorated(*args, **kwargs):
        # <...> 已省略
    return decorated
這樣處理后,timer 裝飾器就不會(huì)影響它所裝飾的函數(shù)了。
print(random_sleep.__name__)
# 輸出 'random_sleep'
print(random_sleep.__doc__)
# 輸出 '隨機(jī)睡眠一小會(huì)'

3. 修改外層變量時(shí)記得使用 nonlocal

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

import functools
def counter(func):
    """裝飾器:記錄并打印調(diào)用次數(shù)"""
    count = 0
    @functools.wraps(func)
    def decorated(*args, **kwargs):
        # 次數(shù)累加
        count += 1
        print(f"Count: {count}")
        return func(*args, **kwargs)
    return decorated
@counter
def foo():
    pass
foo()

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

Traceback (most recent call last):
  File "counter.py", line 22, in 
    foo()
  File "counter.py", line 11, in decorated
    count += 1
UnboundLocalError: local variable 'count' referenced before assignment

這個(gè)錯(cuò)誤是由 counter 與 decorated 函數(shù)互相嵌套的作用域引起的。

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

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

def decorated(*args, **kwargs):
    nonlocal count
    count += 1
    # <... 已省略 ...>

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

總結(jié)

在這篇文章里分享了有關(guān)裝飾器的一些技巧與小知識(shí)。

一些要點(diǎn)總結(jié):

? 一切 callable 的對(duì)象都可以被用來(lái)實(shí)現(xiàn)裝飾器

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

? wrapt 模塊很有用,用它可以幫助我們用更簡(jiǎn)單的代碼寫(xiě)出復(fù)雜裝飾器

? “裝飾器”只是語(yǔ)法糖,它不是“裝飾器模式”

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

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

關(guān)于Python中使用裝飾器的各種技巧就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。


分享文章:Python中使用裝飾器的各種技巧
分享URL:http://weahome.cn/article/gjdgpp.html

其他資訊

在線咨詢(xún)

微信咨詢(xún)

電話咨詢(xún)

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部