Python 中級(jí)知識(shí)之裝飾器诲侮,滾雪球?qū)W Python

橡皮擦镀虐,一個(gè)逗趣的互聯(lián)網(wǎng)高級(jí)網(wǎng)蟲箱蟆,新的系列沟绪,讓我們一起 Be More Pythonic

已完成的文章清單

  1. 滾雪球?qū)W Python 第二輪開啟空猜,進(jìn)階之路绽慈,列表與元組那些事兒
  2. 說完列表說字典,說完字典說集合辈毯,滾雪球?qū)W Python
  3. 關(guān)于 Python 中的字符串坝疼,我在補(bǔ)充兩點(diǎn),滾雪球?qū)W Python
  4. 列表推導(dǎo)式與字典推導(dǎo)式谆沃,滾雪球?qū)W Python
  5. 滾雪球?qū)W Python 之 lambda 表達(dá)式
  6. 滾雪球?qū)W Python 之內(nèi)置函數(shù):filter钝凶、map、reduce唁影、zip耕陷、enumerate

七、函數(shù)裝飾器

裝飾器(Decorators)在 Python 中据沈,主要作用是修改函數(shù)的功能哟沫,而且修改前提是不變動(dòng)原函數(shù)代碼,裝飾器會(huì)返回一個(gè)函數(shù)對(duì)象锌介,所以有的地方會(huì)把裝飾器叫做 “函數(shù)的函數(shù)”嗜诀。
還存在一種設(shè)計(jì)模式叫做 “裝飾器模式”,這個(gè)后續(xù)的課程會(huì)有所涉及孔祸。

裝飾器調(diào)用的時(shí)候隆敢,使用 @,它是 Python 提供的一種編程語(yǔ)法糖崔慧,使用了之后會(huì)讓你的代碼看起來更加 Pythonic拂蝎。

7.1 裝飾器基本使用

在學(xué)習(xí)裝飾器的時(shí)候,最常見的一個(gè)案例尊浪,就是統(tǒng)計(jì)某個(gè)函數(shù)的運(yùn)行時(shí)間匣屡,接下來就為你分享一下封救。
計(jì)算函數(shù)運(yùn)行時(shí)間:

import time

def fun():
    i = 0
    while i < 1000:
        i += 1
def fun1():
    i = 0
    while i < 10000:
        i += 1
s_time = time.perf_counter()
fun()
e_time = time.perf_counter()
print(f"函數(shù){fun.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")

如果你希望給每個(gè)函授都加上調(diào)用時(shí)間,那工作量是巨大的捣作,你需要重復(fù)的修改函數(shù)內(nèi)部代碼誉结,或者修改函數(shù)調(diào)用位置的代碼。在這種需求下券躁,裝飾器語(yǔ)法出現(xiàn)了惩坑。

先看一下第一種修改方法,這種方法沒有增加裝飾器也拜,但是編寫了一個(gè)通用的函數(shù)以舒,利用 Python 中函數(shù)可以作為參數(shù)這一特性,完成了代碼的可復(fù)用性慢哈。

import time
def fun():
    i = 0
    while i < 1000:
        i += 1

def fun1():
    i = 0
    while i < 10000:
        i += 1

def go(fun):
    s_time = time.perf_counter()
    fun()
    e_time = time.perf_counter()
    print(f"函數(shù){fun.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")

if __name__ == "__main__":
    go(fun1)

接下來這種技巧擴(kuò)展到 Python 中的裝飾器語(yǔ)法蔓钟,具體修改如下:

import time

def go(func):
    # 這里的 wrapper 函數(shù)名可以為任意名稱
    def wrapper():
        s_time = time.perf_counter()
        func()
        e_time = time.perf_counter()
        print(f"函數(shù){func.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")
    return wrapper

@go
def func():
    i = 0
    while i < 1000:
        i += 1
@go
def func1():
    i = 0
    while i < 10000:
        i += 1

if __name__ == '__main__':
    func()

在上述代碼中,注意看 go 函數(shù)部分卵贱,它的參數(shù) func 是一個(gè)函數(shù)滥沫,返回值是一個(gè)內(nèi)部函數(shù),執(zhí)行代碼之后相當(dāng)于給原函數(shù)注入了計(jì)算時(shí)間的代碼键俱。在代碼調(diào)用部分兰绣,你沒有做任何修改,函數(shù) func 就具備了更多的功能(計(jì)算運(yùn)行時(shí)間的功能)编振。

裝飾器函數(shù)成功拓展了原函數(shù)的功能缀辩,又不需要修改原函數(shù)代碼,這個(gè)案例學(xué)會(huì)之后踪央,你就已經(jīng)初步了解了裝飾器臀玄。

7.2 對(duì)帶參數(shù)的函數(shù)進(jìn)行裝飾

直接看代碼,了解如何對(duì)帶參數(shù)的函數(shù)進(jìn)行裝飾:

import time

def go(func):
    def wrapper(x, y):
        s_time = time.perf_counter()
        func(x, y)
        e_time = time.perf_counter()
        print(f"函數(shù){func.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")
    return wrapper

@go
def func(x, y):
    i = 0
    while i < 1000:
        i += 1
    print(f"x={x},y={y}")

if __name__ == '__main__':
    func(33, 55)

如果你看著暈乎了杯瞻,我給你標(biāo)記一下參數(shù)的重點(diǎn)傳遞過程镐牺。

20210307130853732[1].png

還有一種情況是裝飾器本身帶有參數(shù),例如下述代碼:

def log(text):
    def decorator(func):
        def wrapper(x):
            print('%s %s():' % (text, func.__name__))
            func(x)
        return wrapper
    return decorator

@log('執(zhí)行')
def my_fun(x):
    print(f"我是 my_fun 函數(shù)魁莉,我的參數(shù) {x}")

my_fun(123)

上述代碼在編寫裝飾器函數(shù)的時(shí)候睬涧,在裝飾器函數(shù)外層又嵌套了一層函數(shù),最終代碼的運(yùn)行順序如下所示:

my_fun = log('執(zhí)行')(my_fun)

此時(shí)如果我們總結(jié)一下旗唁,就能得到結(jié)論了:使用帶有參數(shù)的裝飾器畦浓,是在裝飾器外面又包裹了一個(gè)函數(shù),使用該函數(shù)接收參數(shù)检疫,并且返回一個(gè)裝飾器函數(shù)讶请。
還有一點(diǎn)要注意的是裝飾器只能接收一個(gè)參數(shù),而且必須是函數(shù)類型。

20210307141505987[1].png

7.3 多個(gè)裝飾器

先臨摹一下下述代碼夺溢,再進(jìn)行學(xué)習(xí)與研究论巍。

import time

def go(func):
    def wrapper(x, y):
        s_time = time.perf_counter()
        func(x, y)
        e_time = time.perf_counter()
        print(f"函數(shù){func.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")
    return wrapper

def gogo(func):
    def wrapper(x, y):
        print("我是第二個(gè)裝飾器")
    return wrapper

@go
@gogo
def func(x, y):
    i = 0
    while i < 1000:
        i += 1
    print(f"x={x},y={y}")

if __name__ == '__main__':
    func(33, 55)

代碼運(yùn)行之后,輸出結(jié)果為:

我是第二個(gè)裝飾器
函數(shù)wrapper運(yùn)行時(shí)間是:0.0034401339999999975

雖說多個(gè)裝飾器使用起來非常簡(jiǎn)單风响,但是問題也出現(xiàn)了嘉汰,print(f"x={x},y={y}") 這段代碼運(yùn)行結(jié)果丟失了,這里就涉及多個(gè)裝飾器執(zhí)行順序問題了状勤。

先解釋一下裝飾器的裝飾順序鞋怀。

import time
def d1(func):
    def wrapper1():
        print("裝飾器1開始裝飾")
        func()
        print("裝飾器1結(jié)束裝飾")
    return wrapper1

def d2(func):
    def wrapper2():
        print("裝飾器2開始裝飾")
        func()
        print("裝飾器2結(jié)束裝飾")
    return wrapper2

@d1
@d2
def func():
    print("被裝飾的函數(shù)")

if __name__ == '__main__':
    func()

上述代碼運(yùn)行的結(jié)果為:

裝飾器1開始裝飾
裝飾器2開始裝飾
被裝飾的函數(shù)
裝飾器2結(jié)束裝飾
裝飾器1結(jié)束裝飾

可以看到非常對(duì)稱的輸出,同時(shí)證明被裝飾的函數(shù)在最內(nèi)層持搜,轉(zhuǎn)換成函數(shù)調(diào)用的代碼如下:

d1(d2(func))

你在這部分需要注意的是密似,裝飾器的外函數(shù)內(nèi)函數(shù)之間的語(yǔ)句,是沒有裝飾到目標(biāo)函數(shù)上的葫盼,而是在裝載裝飾器時(shí)的附加操作残腌。
在對(duì)函數(shù)進(jìn)行裝飾的時(shí)候,外函數(shù)與內(nèi)函數(shù)之間的代碼會(huì)被運(yùn)行剪返。

測(cè)試效果如下:

import time

def d1(func):
    print("我是 d1 內(nèi)外函數(shù)之間的代碼")
    def wrapper1():
        print("裝飾器1開始裝飾")
        func()
        print("裝飾器1結(jié)束裝飾")
    return wrapper1

def d2(func):
    print("我是 d2 內(nèi)外函數(shù)之間的代碼")
    def wrapper2():
        print("裝飾器2開始裝飾")
        func()
        print("裝飾器2結(jié)束裝飾")
    return wrapper2

@d1
@d2
def func():
    print("被裝飾的函數(shù)")

運(yùn)行之后废累,你就能發(fā)現(xiàn)輸出結(jié)果如下:

我是 d2 內(nèi)外函數(shù)之間的代碼
我是 d1 內(nèi)外函數(shù)之間的代碼

d2 函數(shù)早于 d1 函數(shù)運(yùn)行邓梅。

接下來在回顧一下裝飾器的概念:
被裝飾的函數(shù)的名字會(huì)被當(dāng)作參數(shù)傳遞給裝飾函數(shù)脱盲。
裝飾函數(shù)執(zhí)行它自己內(nèi)部的代碼后,會(huì)將它的返回值賦值給被裝飾的函數(shù)日缨。

這樣看上文中的代碼運(yùn)行過程是這樣的钱反,d1(d2(func)) 執(zhí)行 d2(func) 之后,原來的 func 這個(gè)函數(shù)名會(huì)指向 wrapper2 函數(shù)匣距,執(zhí)行 d1(wrapper2) 函數(shù)之后面哥,wrapper2 這個(gè)函數(shù)名又會(huì)指向 wrapper1。因此最后的 func 被調(diào)用的時(shí)候毅待,相當(dāng)于代碼已經(jīng)切換成如下內(nèi)容了尚卫。

# 第一步
def wrapper2():
     print("裝飾器2開始裝飾")
     print("被裝飾的函數(shù)")
     print("裝飾器2結(jié)束裝飾")

# 第二步
print("裝飾器1開始裝飾")
wrapper2()
print("裝飾器1結(jié)束裝飾")

# 第三步
def wrapper1():
    print("裝飾器1開始裝飾")
    print("裝飾器2開始裝飾")
    print("被裝飾的函數(shù)")
    print("裝飾器2結(jié)束裝飾")
    print("裝飾器1結(jié)束裝飾")

上述第三步運(yùn)行之后的代碼,恰好與我們的代碼輸出一致尸红。

那現(xiàn)在再回到本小節(jié)一開始的案例吱涉,為何輸出數(shù)據(jù)丟失掉了。

import time

def go(func):
    def wrapper(x, y):
        s_time = time.perf_counter()
        func(x, y)
        e_time = time.perf_counter()
        print(f"函數(shù){func.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")
    return wrapper

def gogo(func):
    def wrapper(x, y):
        print("我是第二個(gè)裝飾器")
    return wrapper

@go
@gogo
def func(x, y):
    i = 0
    while i < 1000:
        i += 1
    print(f"x={x},y={y}")

if __name__ == '__main__':
    func(33, 55)

在執(zhí)行裝飾器代碼裝飾之后外里,調(diào)用 func(33,55) 已經(jīng)切換為 go(gogo(func))怎爵,運(yùn)行 gogo(func) 代碼轉(zhuǎn)換為下述內(nèi)容:

def wrapper(x, y):
    print("我是第二個(gè)裝飾器")

在運(yùn)行 go(wrapper),代碼轉(zhuǎn)換為:

s_time = time.perf_counter()
print("我是第二個(gè)裝飾器")
e_time = time.perf_counter()
print(f"函數(shù){func.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")

此時(shí)盅蝗,你會(huì)發(fā)現(xiàn)參數(shù)在運(yùn)行過程被丟掉了鳖链。

7.4 functools.wraps

使用裝飾器可以大幅度提高代碼的復(fù)用性,但是缺點(diǎn)就是原函數(shù)的元信息丟失了墩莫,比如函數(shù)的 __doc__芙委、__name__

# 裝飾器
def logged(func):
    def logging(*args, **kwargs):
        print(func.__name__)
        print(func.__doc__)
        func(*args, **kwargs)
    return logging

# 函數(shù)
@logged
def f(x):
    """函數(shù)文檔逞敷,說明"""
    return x * x

print(f.__name__) # 輸出 logging
print(f.__doc__) # 輸出 None

解決辦法非常簡(jiǎn)單,導(dǎo)入 from functools import wraps 灌侣,修改代碼為下述內(nèi)容:

from functools import wraps
# 裝飾器
def logged(func):
    @wraps(func)
    def logging(*args, **kwargs):
        print(func.__name__)
        print(func.__doc__)
        func(*args, **kwargs)
    return logging

# 函數(shù)
@logged
def f(x):
    """函數(shù)文檔兰粉,說明"""
    return x * x

print(f.__name__) # 輸出 f
print(f.__doc__)  # 輸出 函數(shù)文檔,說明

7.5 基于類的裝飾器

在實(shí)際編碼中 一般 “函數(shù)裝飾器” 最為常見顶瞳,“類裝飾器” 出現(xiàn)的頻率要少很多玖姑。

基于類的裝飾器與基于函數(shù)的基本用法一致,先看一段代碼:

class H1(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return '<h1>' + self.func(*args, **kwargs) + '</h1>'

@H1
def text(name):
    return f'text {name}'

s = text('class')
print(s)

H1 有兩個(gè)方法:

  • __init__:接收一個(gè)函數(shù)作為參數(shù)慨菱,就是待被裝飾的函數(shù)焰络;
  • __call__:讓類對(duì)象可以調(diào)用,類似函數(shù)調(diào)用符喝,觸發(fā)點(diǎn)是被裝飾的函數(shù)調(diào)用時(shí)觸發(fā)闪彼。

最后在附錄一篇寫的不錯(cuò)的 博客,可以去學(xué)習(xí)协饲。

在這里類裝飾器的細(xì)節(jié)就不在展開了畏腕,等到后面滾雪球相關(guān)項(xiàng)目實(shí)操環(huán)節(jié)再說。

裝飾器為類和類的裝飾器在細(xì)節(jié)上是不同的茉稠,上文提及的是裝飾器為類描馅,你可以在思考一下如何給類添加裝飾器。

7.6 內(nèi)置裝飾器

常見的內(nèi)置裝飾器有 @property而线、@staticmethod铭污、@classmethod。該部分內(nèi)容在細(xì)化面向?qū)ο蟛糠诌M(jìn)行說明膀篮,本文只做簡(jiǎn)單的備注嘹狞。

7.6.1 @property

把類內(nèi)方法當(dāng)成屬性來使用,必須要有返回值誓竿,相當(dāng)于 getter磅网,如果沒有定義 @func.setter 修飾方法,是只讀屬性筷屡。

7.6.2 @staticmethod

靜態(tài)方法涧偷,不需要表示自身對(duì)象的 self 和自身類的 cls 參數(shù),就跟使用函數(shù)一樣速蕊。

7.6.3 @classmethod

類方法嫂丙,不需要 self 參數(shù),但第一個(gè)參數(shù)需要是表示自身類的 cls 參數(shù)规哲。

7.7 這篇博客的總結(jié)

關(guān)于 Python 裝飾器跟啤,網(wǎng)上的文章實(shí)在太太多了,學(xué)習(xí)起來并不是很難,真正難的是恰到好處的應(yīng)用在項(xiàng)目中隅肥,希望本篇博客能對(duì)你理解裝飾器有所幫助竿奏。
其他內(nèi)容也可以查閱 官方手冊(cè)

相關(guān)閱讀

  1. Python 爬蟲 100 例教程腥放,超棒的爬蟲教程泛啸,立即訂閱吧
  2. Python 爬蟲小課,精彩 9 講

今天是持續(xù)寫作的第 <font color="red">103</font> / 200 天秃症。
如果你想跟博主建立親密關(guān)系候址,可以關(guān)注同名公眾號(hào) <font color="red">夢(mèng)想橡皮擦</font>,近距離接觸一個(gè)逗趣的互聯(lián)網(wǎng)高級(jí)網(wǎng)蟲种柑。
博主 ID:夢(mèng)想橡皮擦岗仑,希望大家<font color="red">點(diǎn)贊</font>、<font color="red">評(píng)論</font>聚请、<font color="red">收藏</font>荠雕。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市驶赏,隨后出現(xiàn)的幾起案子炸卑,更是在濱河造成了極大的恐慌,老刑警劉巖煤傍,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盖文,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡患久,警方通過查閱死者的電腦和手機(jī)椅寺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒋失,“玉大人,你說我怎么就攤上這事桐玻「萃欤” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵镊靴,是天一觀的道長(zhǎng)铣卡。 經(jīng)常有香客問我,道長(zhǎng)偏竟,這世上最難降的妖魔是什么煮落? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮踊谋,結(jié)果婚禮上蝉仇,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好轿衔,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布沉迹。 她就那樣靜靜地躺著,像睡著了一般害驹。 火紅的嫁衣襯著肌膚如雪鞭呕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天宛官,我揣著相機(jī)與錄音葫松,去河邊找鬼。 笑死底洗,一個(gè)胖子當(dāng)著我的面吹牛进宝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播枷恕,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼党晋,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了徐块?” 一聲冷哼從身側(cè)響起未玻,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎胡控,沒想到半個(gè)月后扳剿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昼激,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年庇绽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片橙困。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瞧掺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出凡傅,到底是詐尸還是另有隱情辟狈,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布夏跷,位于F島的核電站哼转,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏槽华。R本人自食惡果不足惜壹蔓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望猫态。 院中可真熱鬧佣蓉,春花似錦披摄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至套像,卻和暖如春酿联,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背夺巩。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工贞让, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人柳譬。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓喳张,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親美澳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子销部,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容