目錄
傻瓜解法
函數包裝
通用閉包
語法糖
求值裝飾器
帶參數裝飾器
智能装饰器
首頁 後端開發 Python教學 Python裝飾器的詳細用法介紹(程式碼範例)

Python裝飾器的詳細用法介紹(程式碼範例)

Feb 25, 2019 am 10:33 AM
decorator python 修飾器 裝飾器

本篇文章帶給大家的內容是關於Python裝飾器的詳細用法介紹(程式碼範例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

在Python中,裝飾器一般用來修飾函數,實現公共功能,達到程式碼重複使用的目的。在函數定義前加上@xxxx,然後函數就注入了某些行為,很神奇!然而,這只是語法糖而已。

場景

假設,有一些工作函數,用來對資料做不同的處理:

def work_bar(data):
    pass


def work_foo(data):
    pass
登入後複製

我們想要在函數呼叫前/後輸出日誌,怎麼辦?

傻瓜解法

logging.info('begin call work_bar')
work_bar(1)
logging.info('call work_bar done')
登入後複製

如果有多處程式碼呼叫呢?想想就怕!

函數包裝

傻瓜解法無非是有太多程式碼冗餘,每次函數呼叫都要寫一遍logging。可以把這部分冗餘邏輯封裝到一個新函數裡:

def smart_work_bar(data):
    logging.info('begin call: work_bar')
    work_bar(data)
    logging.info('call doen: work_bar')
登入後複製

這樣,每次調用smart_work_bar#即可:

smart_work_bar(1)

# ...

smart_work_bar(some_data)
登入後複製

通用閉包

看起來挺完美…然而,當work_foo也有同樣的需要時,還要再實現一次smart_work_foo嗎?這樣顯然不科學呀!

別急,我們可以用閉包:

def log_call(func):
    def proxy(*args, **kwargs):
        logging.info('begin call: {name}'.format(name=func.func_name))
        result = func(*args, **kwargs)
        logging.info('call done: {name}'.format(name=func.func_name))
        return result
    return proxy
登入後複製

這個函數接收一個函數物件(被代理函數)作為參數,傳回一個代理函數。呼叫代理函數時,先輸出日誌,再呼叫被代理函數,呼叫完成後再輸出日誌,最後返回呼叫結果。這樣,不就達到通用化的目的了嗎? ——對於任意被代理函數funclog_call皆可輕鬆應付。

smart_work_bar = log_call(work_bar)
smart_work_foo = log_call(work_foo)

smart_work_bar(1)
smart_work_foo(1)

# ...

smart_work_bar(some_data)
smart_work_foo(some_data)
登入後複製

1行中,log_call接收參數work_bar,傳回一個代理函數proxy,並賦給smart_work_bar。在第4行中,呼叫smart_work_bar,也就是代理函數proxy,先輸出日誌,然後呼叫func也就是 work_bar,最後再輸出日誌。注意到,代理函數中,func與傳進去的work_bar物件緊緊關聯在一起了,這就是閉包

再提一下,可以覆寫被代理函數名,以smart_為前綴取新名字還是顯得有些累贅:

work_bar = log_call(work_bar)
work_foo = log_call(work_foo)

work_bar(1)
work_foo(1)
登入後複製

語法糖

#先來看看以下程式碼:

def work_bar(data):
    pass
work_bar = log_call(work_bar)


def work_foo(data):
    pass
work_foo = log_call(work_foo)
登入後複製

雖然程式碼沒有什麼冗餘了,但是看是去還是不夠直覺。這時候,語法糖來了~~~

@log_call
def work_bar(data):
    pass
登入後複製

因此,注意一點(劃重點啦),這裡@log_call的作用只是:告訴 Python編譯器插入程式碼work_bar = log_call(work_bar)

求值裝飾器

先來猜猜裝飾器eval_now有什麼作用?

def eval_now(func):
    return func()
登入後複製

看起來好奇怪哦,沒有定義代理函數,算裝飾器嗎​​?

@eval_now
def foo():
    return 1

print foo
登入後複製

這段程式碼輸出1,也就是對函數進行呼叫求值。那麼到底有什麼用呢?直接寫foo = 1不行麼?在這個簡單的例子,這麼寫當然可以啦。來看一個更複雜的範例-初始化一個日誌物件:

# some other code before...

# log format
formatter = logging.Formatter(
    '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s',
    '%Y-%m-%d %H:%M:%S',
)

# stdout handler
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter)
stdout_handler.setLevel(logging.DEBUG)

# stderr handler
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setFormatter(formatter)
stderr_handler.setLevel(logging.ERROR)

# logger object
logger = logging.Logger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(stdout_handler)
logger.addHandler(stderr_handler)

# again some other code after...
登入後複製

eval_now的方式:

# some other code before...

@eval_now
def logger():
    # log format
    formatter = logging.Formatter(
        '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s',
        '%Y-%m-%d %H:%M:%S',
    )

    # stdout handler
    stdout_handler = logging.StreamHandler(sys.stdout)
    stdout_handler.setFormatter(formatter)
    stdout_handler.setLevel(logging.DEBUG)

    # stderr handler
    stderr_handler = logging.StreamHandler(sys.stderr)
    stderr_handler.setFormatter(formatter)
    stderr_handler.setLevel(logging.ERROR)

    # logger object
    logger = logging.Logger(__name__)
    logger.setLevel(logging.DEBUG)
    logger.addHandler(stdout_handler)
    logger.addHandler(stderr_handler)

    return logger

# again some other code after...
登入後複製

兩段程式碼要達到的目的是一樣的,但後者顯然更清晰,頗有程式碼塊的風範。更重要的是,函數呼叫在局部名字空間完成初始化,避免臨時變數(如formatter等)污染外部的名字空間(如全域)。

帶參數裝飾器

定義一個裝飾器,用來記錄慢函數呼叫:

def log_slow_call(func):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > 1:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy
登入後複製

35行分別在函數呼叫前後取樣目前時間,第7行計算呼叫耗時,耗時大於一秒輸出一條警告日誌。

@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)

sleep_seconds(0.1)  # 没有日志输出

sleep_seconds(2)    # 输出警告日志
登入後複製

然而,閾值設定總是要視情況決定,不同的函數可能會設定不同的值。如果閾值有辦法參數化就好了:

def log_slow_call(func, threshold=1):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > threshold:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy
登入後複製

然而,@xxxx語法糖總是以被裝飾函數為參數呼叫裝飾器,也就是說沒有機會傳遞 threshold參數。怎麼辦呢? ——用一個閉包封裝threshold參數:

def log_slow_call(threshold=1):
    def decorator(func):
        def proxy(*args, **kwargs):
            start_ts = time.time()
            result = func(*args, **kwargs)
            end_ts = time.time()

            seconds = start_ts - end_ts
            if seconds > threshold:
            logging.warn('slow call: {name} in {seconds}s'.format(
                name=func.func_name,
                seconds=seconds,
            ))

            return result

        return proxy

    return decorator


@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
登入後複製

這樣,log_slow_call(threshold=0.5)呼叫回傳函數decorator,函數擁有閉包變數threshold,值為0.5decorator再裝飾sleep_seconds

採用預設閾值,函數呼叫還是不能省略:

@log_slow_call()
def sleep_seconds(seconds):
    time.sleep(seconds)
登入後複製

處女座可能會對第一行這對括號感到不爽,那麼可以這樣改進:

def log_slow_call(func=None, threshold=1):
    def decorator(func):
        def proxy(*args, **kwargs):
            start_ts = time.time()
            result = func(*args, **kwargs)
            end_ts = time.time()

            seconds = start_ts - end_ts
            if seconds > threshold:
            logging.warn('slow call: {name} in {seconds}s'.format(
                name=func.func_name,
                seconds=seconds,
            ))

            return result

        return proxy

    if func is None:
        return decorator
    else:
        return decorator(func)
登入後複製

這種寫法相容於兩種不同的用法,用法A預設閾值(無呼叫);用法B自訂閾值(有呼叫)。

# Case A
@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)


# Case B
@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
登入後複製

用法A中,發生的事情是log_slow_call(sleep_seconds),也就是func參數是非空的,這是直接調decorator進行包裝並返回(閾值是預設的)。

用法B中,先发生的是log_slow_call(threshold=0.5)func参数为空,直接返回新的装饰器decorator,关联闭包变量threshold,值为0.5;然后,decorator再装饰函数sleep_seconds,即decorator(sleep_seconds)。注意到,此时threshold关联的值是0.5,完成定制化。

你可能注意到了,这里最好使用关键字参数这种调用方式——使用位置参数会很丑陋:

# Case B-
@log_slow_call(None, 0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
登入後複製

当然了,函数调用尽量使用关键字参数是一种极佳实践,含义清晰,在参数很多的情况下更是如此。

智能装饰器

上节介绍的写法,嵌套层次较多,如果每个类似的装饰器都用这种方法实现,还是比较费劲的(脑子不够用),也比较容易出错。

假设有一个智能装饰器smart_decorator,修饰装饰器log_slow_call,便可获得同样的能力。这样,log_slow_call定义将变得更清晰,实现起来也更省力啦:

@smart_decorator
def log_slow_call(func, threshold=1):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > threshold:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy
登入後複製

脑洞开完,smart_decorator如何实现呢?其实也简单:

def smart_decorator(decorator):

    def decorator_proxy(func=None, **kwargs):
        if func is not None:
            return decorator(func=func, **kwargs)

        def decorator_proxy(func):
            return decorator(func=func, **kwargs)

        return decorator_proxy

    return decorator_proxy
登入後複製

smart_decorator实现了以后,设想就成立了!这时,log_slow_call,就是decorator_proxy(外层),关联的闭包变量decorator是本节最开始定义的log_slow_call(为了避免歧义,称为real_log_slow_call)。log_slow_call支持以下各种用法:

# Case A
@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)
登入後複製

用法A中,执行的是decorator_proxy(sleep_seconds)(外层),func非空,kwargs为空;直接执行decorator(func=func, **kwargs),即real_log_slow_call(sleep_seconds),结果是关联默认参数的proxy

# Case B
# Same to Case A
@log_slow_call()
def sleep_seconds(seconds):
    time.sleep(seconds)
登入後複製

用法B中,先执行decorator_proxy()funckwargs均为空,返回decorator_proxy对象(内层);再执行decorator_proxy(sleep_seconds)(内层);最后执行decorator(func, **kwargs),等价于real_log_slow_call(sleep_seconds),效果与用法A一致。

# Case C
@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
登入後複製

用法C中,先执行decorator_proxy(threshold=0.5)func为空但kwargs非空,返回decorator_proxy对象(内层);再执行decorator_proxy(sleep_seconds)(内层);最后执行decorator(sleep_seconds, **kwargs),等价于real_log_slow_call(sleep_seconds, threshold=0.5),阈值实现自定义!

以上是Python裝飾器的詳細用法介紹(程式碼範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

PHP和Python:解釋了不同的範例 PHP和Python:解釋了不同的範例 Apr 18, 2025 am 12:26 AM

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

在PHP和Python之間進行選擇:指南 在PHP和Python之間進行選擇:指南 Apr 18, 2025 am 12:24 AM

PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

Python vs. JavaScript:學習曲線和易用性 Python vs. JavaScript:學習曲線和易用性 Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

PHP和Python:深入了解他們的歷史 PHP和Python:深入了解他們的歷史 Apr 18, 2025 am 12:25 AM

PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

vs code 可以在 Windows 8 中運行嗎 vs code 可以在 Windows 8 中運行嗎 Apr 15, 2025 pm 07:24 PM

VS Code可以在Windows 8上運行,但體驗可能不佳。首先確保系統已更新到最新補丁,然後下載與系統架構匹配的VS Code安裝包,按照提示安裝。安裝後,注意某些擴展程序可能與Windows 8不兼容,需要尋找替代擴展或在虛擬機中使用更新的Windows系統。安裝必要的擴展,檢查是否正常工作。儘管VS Code在Windows 8上可行,但建議升級到更新的Windows系統以獲得更好的開發體驗和安全保障。

visual studio code 可以用於 python 嗎 visual studio code 可以用於 python 嗎 Apr 15, 2025 pm 08:18 PM

VS Code 可用於編寫 Python,並提供許多功能,使其成為開發 Python 應用程序的理想工具。它允許用戶:安裝 Python 擴展,以獲得代碼補全、語法高亮和調試等功能。使用調試器逐步跟踪代碼,查找和修復錯誤。集成 Git,進行版本控制。使用代碼格式化工具,保持代碼一致性。使用 Linting 工具,提前發現潛在問題。

notepad 怎麼運行python notepad 怎麼運行python Apr 16, 2025 pm 07:33 PM

在 Notepad 中運行 Python 代碼需要安裝 Python 可執行文件和 NppExec 插件。安裝 Python 並為其添加 PATH 後,在 NppExec 插件中配置命令為“python”、參數為“{CURRENT_DIRECTORY}{FILE_NAME}”,即可在 Notepad 中通過快捷鍵“F6”運行 Python 代碼。

vscode 擴展是否是惡意的 vscode 擴展是否是惡意的 Apr 15, 2025 pm 07:57 PM

VS Code 擴展存在惡意風險,例如隱藏惡意代碼、利用漏洞、偽裝成合法擴展。識別惡意擴展的方法包括:檢查發布者、閱讀評論、檢查代碼、謹慎安裝。安全措施還包括:安全意識、良好習慣、定期更新和殺毒軟件。

See all articles