五分鍾學會Python裝飾器,看完面試不再慌


本文始發於個人公眾號:TechFlow,原創不易,求個關注


今天是Python專題的第12篇文章,我們來看看Python裝飾器。

一段囧事

差不多五年前面試的時候,我就領教過它的重要性。那時候我Python剛剛初學乍練,看完了廖雪峰大神的博客,就去面試了。我應聘的並不是一個Python的開發崗位,但是JD當中寫到了需要熟悉Python。我看網上的面經說到Python經常會問裝飾器,我當時想的是裝飾器我已經看過了,應該問題不大……

沒想到面試的時候還真的問到了,面試官問我Python當中的裝飾器是什么。由於緊張和遺忘,我支支吾吾了半天也沒答上來。我隱約聽到了電話那頭的一聲嘆息……

時隔多年,我已經不記得那是一家什么公司了(估計規模也不大),但裝飾器很重要這個事情給我深深打下了烙印。

裝飾器本質

如今如果再有面試官問我Python中的裝飾器是什么,我一句話就能給回答了,倒不是我裝逼,實際上也的確只需要一句話。Python中的裝飾器,本質上就是一個高階函數

你可能不太清楚高階函數的定義,沒關系,我們可以類比一下。在數學當中高階導數,比如二次導數,表示導數的導數。那么這里高階函數自然就是函數的函數,結合我們之前介紹過的函數式編程,也就是說是一個返回值是函數的函數。但是這個定義是充分不必要的,也就是說裝飾器是高階函數,但是高階函數並不都是裝飾器。裝飾器是高階函數一種特殊的用法。

任意參數

在介紹裝飾器的具體使用之前,我們先來了解和熟悉一下Python當中的任意參數。

Python當中支持任意參數,它寫成*args, **kw。表示的含義是接受任何形式的參數

舉個例子,比如我們定義一個函數:

def exp(a, b, c='3', d='f'):
    print(a, b, c, d)

我們可以這樣調用:

args = [13]
dt = {'c'4'd'5}

exp(*args, **dt)

最后輸出的結果是1, 3, 4, 5。也就是說我們用一個list和dict可以表示任何參數。因為Python當中規定必選參數一定寫在可選參數的前面,而必選參數是可以不用加上名稱標識的,也就是可以不用寫a=1,直接傳入1即可。那么這些沒有名稱標識的必選參數就可以用一個list來表示,而可選參數是必須要加上名稱標識的,這些參數可以用dict來表示,這兩者相加可以表示任何形式的參數。

注意我們傳入list和dict的時候前面加上了*和**,它表示將list和dict當中的所有值展開。如果不加的話,list和dict會被當成是整體傳入。

所以如果一個函數寫成這樣,它表示可以接受任何形式的參數。

def exp(*args, **kw):
    pass

定義裝飾器

明白了任意參數的寫法之后,裝飾器就不難了。

既然我們可以用*args, **kw接受任何參數。並且Python當中支持一個函數作為參數傳入另外一個函數,如果我們把函數和這個函數的所有參數全部傳入另外一個函數,那么不就可以實現代理了嗎?

還是剛才的例子,我們額外增加一個函數:


def exp(a, b, c='3', d='f'):
    print(a, b, c, d)

def agent(func, *args, **kwargs):
    func(*args, **kwargs)

args = [1]
dt = {'b'1'c'4'd'5}

agent(exp, *args, **dt)

裝飾器的本質其實就是這樣一個agent函數,但是如果使用的時候需要手動傳入會非常麻煩,使用起來不太方便。所以Python當中提供了特定的庫,我們可以讓裝飾器以注解的方式使用,大大簡化操作:

from functools import wraps

def wrapexp(func):
    def wrapper(*args, **kwargs):
        print('this is a wrapper')
        func(*args, **kwargs)
    return wrapper


@wrapexp
def exp(a, b, c='3', d='f'):
    print(a, b, c, d)


args = [13]
dt = {'c'4'd'5}

exp(*args, **dt)

在這個例子當中,我們定義了一個wrapexp的裝飾器。我們在其中的wrapper方法當中實現了裝飾器的邏輯,wrapexp當中傳入的參數func是一個函數,wrapper當中的參數則是func的參數。所以我們在wrapper當中調用func(*args, **kw),就是調用打上了這個注解的函數本身。比如在這個例子當中,我們沒有做任何事情,只是在原樣調用之前多輸出了一行’this is a wrapper',表示我們的裝飾器調用成功了。

裝飾器用途

我們理解了裝飾器的基本使用方法之后,自然而然地會問一個天然的問題,學會了它究竟有什么用呢?

如果你從上面的例子當中沒有領會到裝飾器的強大,不如讓我用一個例子再來暗示一下。比如說你是一個程序員,辛辛苦苦做出了一個功能,寫了好幾千行代碼,上百個函數,終於通過了審核上線了。這個時候,你的產品經理找到了你說,經過分析我們發現上線的功能運行速度不達標,經常有請求超時,你能不能計算一下每個函數運行的耗時,方便我們找到需要優化的地方?

這是一個非常合理的請求,但想想看你寫了上百個函數,如果每一個函數都要手動添加時間計算,這要寫多少代碼?萬一哪個函數不小心改錯了,你又得一一檢查,並且如果要求嚴格的話你還得為每一個函數專門寫一個單元測試……

我想,正常的程序員應該都會抗拒這個需求。

但是有了裝飾器就很簡單了,我們可以實現一個計算函數耗時的裝飾器,然后我們只需要給每一個函數加上注解就好了。

import time
from functools import wraps
def timethis(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end-start)
        return result
    return wrapper

這也是裝飾器最大的用途,可以在不修改函數內部代碼的前提下,為它包裝一些額外的功能。

元信息

我們之前說過裝飾器的本質是高階函數,所以我們也可以和高階函數一樣來調用裝飾器,比如下面這樣:

def exp(a, b, c='3', d='f'):
    print(a, b, c, d)


args = [13]
dt = {'c'4'd'5}

f = wrapexp(exp)
f(*args, **dt)

這樣的方式得到的結果和使用注解是一樣的,也就是說我們加上注解的本質其實就是調用裝飾器返回一個新的函數。

既然和高階函數是一樣的,那么就帶來了一個問題,我們使用的其實已經不再是原函數了,而是一個由裝飾器返回的新函數,雖然這個函數的功能和原函數一樣,但是一些基礎的信息其實已經丟失了。

比如我們可以打印出函數的name來做個實驗:

正常的函數調用__name__返回的都是函數的名稱,但是當我們加上了裝飾器的注解之后,就會發生變化,同樣,我們輸出加上了裝飾器注解之后的結果:

我們會發現輸出的結果變成了wrapper,這是因為我們實現的裝飾器內部的函數叫做wrapper。不僅僅是__name__,函數內部還有很多其他的基本信息,比如記錄函數內描述的__doc__,__annotations__等等,這些基本信息被稱為是元信息,這些元信息由於我們使用注解發生了丟失。

有沒有什么辦法可以保留這些函數的元信息呢?

其實很簡單,Python當中為我們提供了一個專門的裝飾器用來保留函數的元信息,我們只需要在實現裝飾器的wrapper函數當中加上一個注解wraps即可。

def wrapexp(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('this is a wrapper')
        func(*args, **kwargs)
    return wrapper

加上了這個注解之后,我們再來檢查函數的元信息,會發現它和我們預期一致了。

總結

了解了Python中的裝飾器之后,再來看之前我們用過的@property, @staticmethod等注解,想必都能明白,它們背后的實現其實也是裝飾器。靈活使用裝飾器可以大大簡化我們的代碼,讓我們的代碼更加規范簡潔,還能靈活地實現一些特殊的功能。

裝飾器的用法很多,今天介紹的只是其中最基本的,在后續的文章當中,還會繼續和大家分享它更多其他的用法。在文章開始的時候我也說了,裝飾器是Python進階必學的技能之一。想要熟練掌握這門語言,靈活運用,看懂大佬的源碼,裝飾器是必須會的東西。

希望大家都能有所收獲,原創不易,厚顏求個贊和關注~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM