python函數裝飾器詳解

来源:https://www.cnblogs.com/f-ck-need-u/archive/2018/12/29/10198247.html
-Advertisement-
Play Games

基礎:函數裝飾器的表現方式 假如你已經定義了一個函數funcA(),在準備定義函數funcB()的時候,如果寫成下麵的格式: 表示用函數funcA()裝飾函數funcB()。當然,也可以認為是funcA包裝函數funcB。它等價於: 也就是說,將函數funcB作為函數funcA的參數,funcA會重 ...


先混個眼熟

誰可以作為裝飾器(可以將誰編寫成裝飾器):

  1. 函數
  2. 方法
  3. 實現了__call__的可調用類

裝飾器可以去裝飾誰(誰可以被裝飾):

  1. 函數
  2. 方法

基礎:函數裝飾器的表現方式

假如你已經定義了一個函數funcA(),在準備定義函數funcB()的時候,如果寫成下麵的格式:

@funcA
def funcB():...

表示用函數funcA()裝飾函數funcB()。當然,也可以認為是funcA包裝函數funcB。它等價於:

def funcB():...

funcB = funcA(funcB)

也就是說,將函數funcB作為函數funcA的參數,funcA會重新返回另一個可調用的對象(比如函數)並賦值給funcB。

所以,funcA要想作為函數裝飾器,需要接收函數作為參數,並且返回另一個可調用對象(如函數)。例如:

def funcA(F):
    ...
    ...
    return Callable

註意,函數裝飾器返回的可調用對象並不一定是原始的函數F,可以是任意其它可調用對象,比如另一個函數。但最終,這個返回的可調用對象都會被賦值給被裝飾的函數變數(上例中的funcB)。

函數可以同時被多個裝飾器裝飾,後面的裝飾器以前面的裝飾器處理結果為基礎進行處理:

@decorator1
@decorator2
def func():...

# 等價於
func = decorator1(decorator2(func))

當調用被裝飾後的funcB時,將自動將funcB進行裝飾,並調用裝飾後的對象。所以,下麵是等價的調用方式:

funcB()          # 調用裝飾後的funcB
funcA(funcB)()

瞭解完函數裝飾器的表現後,大概也能猜到了,裝飾器函數可以用來擴展、增強另外一個函數。實際上,內置函數中staticmethod()、classmethod()和property()都是裝飾器函數,可以用來裝飾其它函數,在後面會學到它們的用法。

兩個簡單的例子

例如,函數f()返回一些字元串,現在要將它的返回結果轉換為大寫字母。可以定義一個函數裝飾器來增強函數f()。

def toupper(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@toupper
def f(x: str):    # 等價於f = toupper(f)
    return x

res = f("abcd")
print(res)

上面toupper()裝飾f()後,調用f("abcd")的時候,等價於執行toupper(f)("abcd"),參數"abcd"傳遞給裝飾器中的wrapper()中的*args,在wrapper中又執行了f("abcd"),使得原本屬於f()的整個過程都完整了,最後返回result.upper(),這部分是對函數f()的擴展部分。

註意,上面的封裝函數wrapper()中使用了*args **kwargs,是為了確保任意參數的函數都能正確執行下去。

再比如要計算一個函數autodown()的執行時長,可以額外定義一個函數裝飾器timecount()。

import time

# 函數裝飾器
def timecount(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end - start)
        return result
    return wrapper

# 裝飾函數
@timecount
def autodown(n: int):
    while n > 0:
        n -= 1

# 調用被裝飾的函數
autodown(100000)
autodown(1000000)
autodown(10000000)

執行結果:

autodown 0.004986763000488281
autodown 0.05684685707092285
autodown 0.5336081981658936

上面wrapper()中的return是多餘的,是因為這裡裝飾的autodown()函數自身沒有返回值。但卻不應該省略這個return,因為timecount()可以去裝飾其它可能有返回值的函數。

@functools.wraps

前面的裝飾器代碼邏輯上沒有什麼問題,但是卻存在隱藏的問題:函數的元數據信息丟了。比如doc、註解等。

比如下麵的代碼:

@timecount
def autodown(n: int):
    ''' some docs '''
    while n > 0:
        n -= 1

print(autodown.__name__)
print(autodown.__doc__)
print(autodown.__annotations__)

執行結果為:

wrapper
None
{}

所以,必須要將被裝飾函數的元數據保留下來。可以使用functools模塊中的wraps()裝飾一下裝飾器中的wrapper()函數。如下:

import time
from functools import wraps

def timecount(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end - start)
        return result
    return wrapper

現在,再去查看autodown函數的元數據信息,將會得到被保留下來的內容:

autodown
 some doc
{'n': <class 'int'>}

所以,wraps()的簡單用法是:向wraps()中傳遞的func參數,那麼func的元數據就會被保留下來。

上面@wraps(func)裝飾wrapper的過程等價於:

def wrapper(*args, **kwargs):...
wrapper = wraps(func)(wrapper)

請註意這一點,因為在將類作為裝飾器的時候,經常會在__init__(self, func)里這樣使用:

class cls:
    def __init__(self, func):
        wraps(func)(self)
        ...
    def __call__(self, *args, **kwargs):
        ...

解除裝飾

函數被裝飾後,如何再去訪問未被裝飾狀態下的這個函數?@wraps還有一個重要的特性,可以通過被裝飾對象的__wrapped__屬性來直接訪問被裝飾對象。例如:

autodown.__wrapped__(1000000)

new_autodown = autodown.__wrapped__
new_autodown(1000000)

上面的調用不會去調用裝飾後的函數,所以不會輸出執行時長。

註意,如果函數被多個裝飾器裝飾,那麼通過__wrapped__,將只會解除第一個裝飾過程。例如:

@decorator1
@decorator2
@decorator3
def f():...

當訪問f.__wrapped__()的時候,只有decorator1被解除,剩餘的所有裝飾器仍然有效。註意,python 3.3之前是略過所有裝飾器。

下麵是一個多裝飾的示例:

from functools import wraps


def decorator1(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("in decorator1")
        return func(*args, **kwargs)
    return wrapper


def decorator2(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("in decorator2")
        return func(*args, **kwargs)
    return wrapper


def decorator3(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("in decorator3")
        return func(*args, **kwargs)
    return wrapper


@decorator1
@decorator2
@decorator3
def addNum(x, y):
    return x+y

返回結果:

in decorator1
in decorator2
in decorator3
5
in decorator2
in decorator3
5

如果不使用functools的@wraps的__wrapped__,想要手動去引用原始函數,需要做的工作可能會非常多。所以,如有需要,直接使用__wrapped__去調用未被裝飾的函數比較好。

另外,並不是所有裝飾器中都使用了@wraps

帶參數的函數裝飾器

函數裝飾器也是可以帶上參數的。

@decorator(x,y,z)
def func():...

它等價於:

func = decorator(x,y,z)(func)

它並不是"天生"就這樣等價的,而是根據編碼規範編寫裝飾器的時候,通常會這樣。其實帶參數的函數裝飾器寫起來有點繞:先定義一個帶有參數的外層函數,它是外在的函數裝飾器,這個函數內包含了真正的裝飾器函數,而這個內部的函數裝飾器的內部又包含了被裝飾的函數封裝。也就是函數嵌套了一次又一次。

所以,結構大概是這樣的:

def out_decorator(some_args):
    ...SOME CODE...
    def real_decorator(func):
        ...SOME CODE...
        def wrapper(*args, **kwargs):
            ...SOME CODE WITH func...
        return wrapper
    return real_decorator

# 等價於func = out_decorator(some_args)(func)
@out_decorator(some_args)
def func():...     

下麵是一個簡單的例子:

from functools import wraps

def out_decorator(x, y, z):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(x)
            print(y)
            print(z)
            return func(*args, **kwargs)
        return wrapper
    return decorator


@out_decorator("xx", "yy", "zz")
def addNum(x, y):
    return x+y

print(addNum(2, 3))

參數隨意的裝飾器

根據前面介紹的兩種情況,裝飾器可以帶參數、不帶參數,所以有兩種裝飾的方式,要麼是下麵的(1),要麼是下麵的(2)。

@decorator         # (1)
@decorator(x,y,z)  # (2)

所以,根據不同的裝飾方式,需要編寫是否帶參數的不同裝飾器。

但是現在想要編寫一個將上面兩種參數方式統一起來的裝飾器。

可能第一想法是讓裝飾器參數預設化:

def out_decorator(arg1=X, arg2=Y...):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            ...
        return wrapper
    return decorator

現在可以用下麵兩種方式來裝飾:

@out_decorator()
@out_decorator(arg1,arg2)

雖然上面兩種裝飾方式會正確進行,但這並非合理做法,因為下麵這種最通用的裝飾方式會錯誤:

@out_decorator

為瞭解決這個問題,回顧下前面裝飾器是如何等價的:

# 等價於 func = decorator(func)
@decorator
def func():...

# 等價於 func = out_decorator(x, y, z)(func)
@out_decorator(x, y, z)
def func():...

上面第二種方式中,out_decorator(x,y,z)才是真正返回的內部裝飾器。所以,可以修改下裝飾器的編寫方式,將func也作為out_decorator()的其中一個參數:

from functools import wraps,partial

def decorator(func=None, arg1=X, arg2=Y):
    # 如果func為None,說明觸發的帶參裝飾器
    # 直接返回partial()封裝後的裝飾器函數
    if func is None:
        decorator_new = partial(decorator, arg1=arg1, arg2=arg2)
        return decorator_new
        #return partial(decorator, arg1=arg1, arg2=arg2)
    
    # 下麵是裝飾器的完整裝飾內容
    @wraps(func)
    def wrapper(*args, **kwargs):
        ...
    return wrapper

上面使用了functools模塊中的partial()函數,它可以返回一個新的將某些參數"凍結"後的函數,使得新的函數無需指定這些已被"凍結"的參數,從而減少參數的數量。如果不知道這個函數,參考partial()用法說明

現在,可以統一下麵3種裝飾方式:

@decorator()
@decorator(arg1=x,arg2=y)
@decorator

前兩種裝飾方式,等價的調用方式是decorator()(func)decorator(arg1=x,arg2=y)(func),它們的func都為None,所以都會通過partial()返回通常的裝飾方式@decorator所等價的形式。

需要註意的是,因為上面的參數結構中包含了func=None作為第一個參數,所以帶參數裝飾時,必須使用keyword格式來傳遞參數,不能使用位置參數。

下麵是一個簡單的示例:

from functools import wraps, partial


def decorator(func=None, x=1, y=2, z=3):
    if func is None:
        return partial(decorator, x=x, y=y, z=z)

    @wraps(func)
    def wrapper(*args, **kwargs):
        print("x: ", x)
        print("y: ", y)
        print("z: ", z)
        return func(*args, **kwargs)
    return wrapper

下麵3種裝飾方式都可以:

@decorator
def addNum(a, b):
    return a + b
print(addNum(2, 3))

print("=" * 40)

@decorator()
def addNum(a, b):
    return a + b
print(addNum(2, 3))

print("=" * 40)

# 必須使用關鍵字參數進行裝飾
@decorator(x="xx", y="yy", z="zz")
def addNum(a, b):
    return a + b
print(addNum(2, 3))

返回結果:

x:  1
y:  2
z:  3
5
====================
x:  1
y:  2
z:  3
5
====================
x:  xx
y:  yy
z:  zz
5

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 雇工模式 雇工模式也叫僕人模式, 其意圖為: 雇工模式是行為模式的一種, 它為一組類提供通用的功能,而不需要類實現這些功能,它是命令模式的一種擴展. 其類圖如下: 其中角色如下: IServiced 代碼如下: Serviced1 和 Serviced2 實現了 IServiced, 代碼如下: S ...
  • 系統介紹: 1.系統採用主流的 SSM 框架 jsp JSTL bootstrap html5 (PC瀏覽器使用) 2.springmvc +spring4.3.7+ mybaits3.3 SSM 普通java web(非maven, 附贈pom.xml文件) 資料庫:mysql 3.開發工具:my ...
  • 行為類模式包括: 行為型模式涉及到演算法和對象間職責的分配 行為類模式關註如何管理對象的行為. 命令模式VS策略模式 策略模式和命令模式相似, 特別是命令模式退化時,比如無接收者(接收者非常簡單或者接收者是Java的基礎操作,無需專門寫一個接收者), 這種情況下, 命令模式和策略模式的類圖完全一樣. ...
  • 規格模式 規格模式 使用了策略模式,組合模式. 只不過將模式具體化了 規格模式將 與或非 操作進行了封裝, 實現了類似 SQL查詢語句的操作. 類圖如下: 其中的角色如下: 抽象規格書代碼: 組合規格書代碼: 與或非規格書代碼: 業務規格書: 場景類: 規格模式已經是一個非常具體的應用框架了, 遇到 ...
  • 資料庫的讀寫操作中,事務在保證數據的安全性和一致性方面起著關鍵的作用,而回滾正是這裡面的核心操作。Django的ORM在事務方面也提供了不少的API。有事務出錯的整體回滾操作,也有基於保存點的部分回滾。本文將討論Django中的這兩種機制的運行原理。 Django利用django.db.transa ...
  • 繼承與多態 引用有的時候並不能達到多態的效果 問題:c++ primer 第五版說,只有指針和引用調用虛函數時才會發生動態綁定(多態)。實踐一下,發現引用有的時候不能發生多態綁定(多態)。 下麵的例子,父類是Quote,在Quote里定義了一個虛函數debug,用來列印出各自成員的值。2個子類Bul ...
  • 寫這篇博客其實主要算是一個筆記,當然也是為了希望幫助更多使用git的新手 1、首先,你需要在本地安裝git客戶端,此處網上教程較多,不在贅述 2、假設本地項目路徑為: E:\gitWorkspace\spring-framework-5.1.2.RELEASE 3、創建本地git倉庫 進入到 spr ...
  • 讀入 n(>0)名學生的姓名、學號、成績,分別輸出成績最高和成績最低學生的姓名和學號。 輸入格式: 每個測試輸入包含 1 個測試用例,格式為 其中姓名和學號均為不超過 10 個字元的字元串,成績為 0 到 100 之間的一個整數,這裡保證在一組測試用例中沒有兩個學生的成績是相同的。 輸出格式: 對每 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...