首頁 後端開發 Python教學 詳解Python的裝飾器

詳解Python的裝飾器

Nov 01, 2016 am 11:24 AM
python

Python中的裝飾器是你進入Python大門的一道坎,不管你跨不跨過去它都在那裡。

為什麼需要裝飾器

我們假設你的程式實作了say_hello()和say_goodbye()兩個函式。

def say_hello(): 
    print "hello!" 
     
def say_goodbye(): 
    print "hello!"  # bug here 
 
if __name__ == '__main__': 
    say_hello() 
    say_goodbye()
登入後複製

但是在實際呼叫中,我們發現程式出錯了,上面的程式碼印了兩個hello。經過調試你發現say_goodbye()出錯了。老闆要求呼叫每個方法前都要記錄進入函數的名稱,例如這樣:

[DEBUG]: Enter say_hello() 
Hello! 
[DEBUG]: Enter say_goodbye() 
Goodbye!
登入後複製

好,小A是個畢業生,他是這樣實現的。

def say_hello(): 
    print "[DEBUG]: enter say_hello()" 
    print "hello!" 
 
def say_goodbye(): 
    print "[DEBUG]: enter say_goodbye()" 
    print "hello!" 
 
if __name__ == '__main__': 
    say_hello() 
    say_goodbye()
登入後複製

很low吧? 嗯是的。小B工作有一段時間了,他告訴小A可以這樣寫。

def debug(): 
    import inspect 
    caller_name = inspect.stack()[1][3] 
    print "[DEBUG]: enter {}()".format(caller_name)    
 
def say_hello(): 
    debug() 
    print "hello!" 
 
def say_goodbye(): 
    debug() 
    print "goodbye!" 
 
if __name__ == '__main__': 
    say_hello() 
    say_goodbye()
登入後複製

是不是好一點?那當然,但是每個業務函數裡都要調用一下debug()函數,是不是很難受?萬一老闆說say相關的函數不用debug,do相關的才需要呢?

那麼裝飾器這時候應該登場了。

裝飾器本質上是一個Python函數,它可以讓其他函數在不需要做任何程式碼變動的前提下增加額外功能,裝飾器的回傳值也是一個函數物件。它常用於有切面需求的場景,例如:插入日誌、效能測試、交易處理、快取、權限校驗等場景。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量與函數功能本身無關的雷同程式碼並繼續重複使用。

概括的講,裝飾器的作用就是為已經存在的函數或物件添加額外的功能。

怎麼寫一個裝飾器

在早些時候 (Python Version < 2.4,2004年以前),為一個函數添加額外功能的寫法是這樣的。 < 2.4,2004年以前),为一个函数添加额外功能的写法是这样的。

def debug(func): 
    def wrapper(): 
        print "[DEBUG]: enter {}()".format(func.__name__) 
        return func() 
    return wrapper 
 
def say_hello(): 
    print "hello!" 
 
say_hello = debug(say_hello)  # 添加功能并保持原函数名不变
登入後複製

上面的debug函數其實已經是一個裝飾器了,它對原函數做了包裝並返回了另外一個函數,額外添加了一些功能。因為這樣寫實在不太優雅,在後面版本的Python中支援了@語法糖,下面程式碼等同於早期的寫法。

def debug(func): 
    def wrapper(): 
        print "[DEBUG]: enter {}()".format(func.__name__) 
        return func() 
    return wrapper 
 
@debug 
def say_hello(): 
    print "hello!"
登入後複製

這是最簡單的裝飾器,但是有一個問題,如果被裝飾的函數需要傳入參數,那麼這個裝飾器就壞了。因為傳回的函數並不能接受參數,你可以指定裝飾器函數wrapper接受和原函數一樣的參數,例如:

def debug(func): 
    def wrapper(something):  # 指定一毛一样的参数 
        print "[DEBUG]: enter {}()".format(func.__name__) 
        return func(something) 
    return wrapper  # 返回包装过函数 
 
@debug 
def say(something): 
    print "hello {}!".format(something)
登入後複製

這樣你就解決了一個問題,但又多了N個問題。因為函數有千千萬,你只管你自己的函數,別人的函數參數是什麼樣子,鬼知道?還好Python提供了可變參數*args和關鍵字參數**kwargs,有了這兩個參數,裝飾器就可以用於任意目標函數了

def debug(func): 
    def wrapper(*args, **kwargs):  # 指定宇宙无敌参数 
        print "[DEBUG]: enter {}()".format(func.__name__) 
        print &#39;Prepare and say...&#39;, 
        return func(*args, **kwargs) 
    return wrapper  # 返回 
 
@debug 
def say(something): 
    print "hello {}!".format(something)
登入後複製

至此,你已完全掌握初級的裝飾器寫法。

高級一點的裝飾器

帶參數的裝飾器和類別裝飾器屬於進階的內容。在理解這些裝飾器之前,最好先對函數的閉包和裝飾器的介面約定有一定了解。 (參見http://betacat.online/posts/p...

帶參數的裝飾器

假設我們前文的裝飾器需要完成的功能不僅僅是能在進入某個函數後打出log信息,而且還需指定log的級別,那麼裝飾器就會是這樣的。 ='DEBUG'),它其實是一個函數,會馬上被執行,只要這個它回傳的結果是一個裝飾器時,那就沒問題。裝飾器函數其實是這樣一個介面約束,它必須接受一個callable物件作為參數,然後回傳一個callable物件。方法,那麼這個物件就是callable的。的內部行為。後文)。

def logging(level): 
    def wrapper(func): 
        def inner_wrapper(*args, **kwargs): 
            print "[{level}]: enter function {func}()".format( 
                level=level, 
                func=func.__name__) 
            return func(*args, **kwargs) 
        return inner_wrapper 
    return wrapper 
 
@logging(level=&#39;INFO&#39;) 
def say(something): 
    print "say {}!".format(something) 
 
# 如果没有使用@语法,等同于 
# say = logging(level=&#39;INFO&#39;)(say) 
 
@logging(level=&#39;DEBUG&#39;) 
def do(something): 
    print "do {}...".format(something) 
 
if __name__ == &#39;__main__&#39;: 
    say(&#39;hello&#39;) 
    do("my work")
登入後複製

帶參數的類別裝飾器

如果需要透過類別形式實現帶有參數的裝飾器,那麼會比前面的例子稍微複雜一點。參數。的,只不過返回的不是函數,而是類對象,所以更難理解一些。

def getx(self): 
    return self._x 
 
def setx(self, value): 
    self._x = value 
     
def delx(self): 
   del self._x 
 
# create a property 
x = property(getx, setx, delx, "I am doc for x property")
登入後複製

以上就是一个Python属性的标准写法,其实和Java挺像的,但是太罗嗦。有了@语法糖,能达到一样的效果但看起来更简单。

@property 
def x(self): ... 
 
# 等同于 
 
def x(self): ... 
x = property(x)
登入後複製

属性有三个装饰器:setter, getter, deleter ,都是在property()的基础上做了一些封装,因为setter和deleter是property()的第二和第三个参数,不能直接套用@语法。getter装饰器和不带getter的属性装饰器效果是一样的,估计只是为了凑数,本身没有任何存在的意义。经过@property装饰过的函数返回的不再是一个函数,而是一个property对象。

>>> property() 
<property object at 0x10ff07940>
登入後複製

@staticmethod,@classmethod

有了@property装饰器的了解,这两个装饰器的原理是差不多的。@staticmethod返回的是一个staticmethod类对象,而@classmethod返回的是一个classmethod类对象。他们都是调用的是各自的__init__()构造函数。

class classmethod(object): 
    """ 
    classmethod(function) -> method 
    """     
    def __init__(self, function): # for @classmethod decorator 
        pass 
    # ... 
class staticmethod(object): 
    """ 
    staticmethod(function) -> method 
    """ 
    def __init__(self, function): # for @staticmethod decorator 
        pass 
    # ...
登入後複製

装饰器的@语法就等同调用了这两个类的构造函数。

class Foo(object): 
 
    @staticmethod 
    def bar(): 
        pass 
     
    # 等同于 bar = staticmethod(bar)
登入後複製

至此,我们上文提到的装饰器接口定义可以更加明确一些,装饰器必须接受一个callable对象,其实它并不关心你返回什么,可以是另外一个callable对象(大部分情况),也可以是其他类对象,比如property。

装饰器里的那些坑

装饰器可以让你代码更加优雅,减少重复,但也不全是优点,也会带来一些问题。

位置错误的代码

让我们直接看示例代码。

def html_tags(tag_name): 
    print &#39;begin outer function.&#39; 
    def wrapper_(func): 
        print "begin of inner wrapper function." 
        def wrapper(*args, **kwargs): 
            content = func(*args, **kwargs) 
            print "<{tag}>{content}</{tag}>".format(tag=tag_name, content=content) 
        print &#39;end of inner wrapper function.&#39; 
        return wrapper 
    print &#39;end of outer function&#39; 
    return wrapper_ 
 
@html_tags(&#39;b&#39;) 
def hello(name=&#39;Toby&#39;): 
    return &#39;Hello {}!&#39;.format(name) 
 
hello() 
hello()
登入後複製

在装饰器中我在各个可能的位置都加上了print语句,用于记录被调用的情况。你知道他们最后打印出来的顺序吗?如果你心里没底,那么最好不要在装饰器函数之外添加逻辑功能,否则这个装饰器就不受你控制了。以下是输出结果:

begin outer function. 
end of outer function 
begin of inner wrapper function. 
end of inner wrapper function. 
<b>Hello Toby!</b> 
<b>Hello Toby!</b>
登入後複製

错误的函数签名和文档

装饰器装饰过的函数看上去名字没变,其实已经变了。

def logging(func): 
    def wrapper(*args, **kwargs): 
        """print log before a function.""" 
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) 
        return func(*args, **kwargs) 
    return wrapper 
 
@logging 
def say(something): 
    """say something""" 
    print "say {}!".format(something) 
 
print say.__name__  # wrapper
登入後複製

为什么会这样呢?只要你想想装饰器的语法糖@代替的东西就明白了。@等同于这样的写法。

say = logging(say)
登入後複製

logging其实返回的函数名字刚好是wrapper,那么上面的这个语句刚好就是把这个结果赋值给say,say的__name__自然也就是wrapper了,不仅仅是name,其他属性也都是来自wrapper,比如doc,source等等。

使用标准库里的functools.wraps,可以基本解决这个问题。

from functools import wraps 
 
def logging(func): 
    @wraps(func) 
    def wrapper(*args, **kwargs): 
        """print log before a function.""" 
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) 
        return func(*args, **kwargs) 
    return wrapper 
 
@logging 
def say(something): 
    """say something""" 
    print "say {}!".format(something) 
 
print say.__name__  # say 
print say.__doc__ # say something
登入後複製

看上去不错!主要问题解决了,但其实还不太完美。因为函数的签名和源码还是拿不到的。

import inspect 
print inspect.getargspec(say)  # failed 
print inspect.getsource(say)  # failed
登入後複製

如果要彻底解决这个问题可以借用第三方包,比如wrapt。后文有介绍。

不能装饰@staticmethod 或者 @classmethod

当你想把装饰器用在一个静态方法或者类方法时,不好意思,报错了。

class Car(object): 
    def __init__(self, model): 
        self.model = model 
 
    @logging  # 装饰实例方法,OK 
    def run(self): 
        print "{} is running!".format(self.model) 
 
    @logging  # 装饰静态方法,Failed 
    @staticmethod 
    def check_model_for(obj): 
        if isinstance(obj, Car): 
            print "The model of your car is {}".format(obj.model) 
        else: 
            print "{} is not a car!".format(obj) 
 
""" 
Traceback (most recent call last): 
... 
  File "example_4.py", line 10, in logging 
    @wraps(func) 
  File "C:\Python27\lib\functools.py", line 33, in update_wrapper 
    setattr(wrapper, attr, getattr(wrapped, attr)) 
AttributeError: &#39;staticmethod&#39; object has no attribute &#39;__module__&#39; 
"""
登入後複製

前面已经解释了@staticmethod这个装饰器,其实它返回的并不是一个callable对象,而是一个staticmethod对象,那么它是不符合装饰器要求的(比如传入一个callable对象),你自然不能在它之上再加别的装饰器。要解决这个问题很简单,只要把你的装饰器放在@staticmethod之前就好了,因为你的装饰器返回的还是一个正常的函数,然后再加上一个@staticmethod是不会出问题的。

class Car(object): 
    def __init__(self, model): 
        self.model = model 
 
    @staticmethod 
    @logging  # 在@staticmethod之前装饰,OK 
    def check_model_for(obj): 
        pass
登入後複製

如何优化你的装饰器

嵌套的装饰函数不太直观,我们可以使用第三方包类改进这样的情况,让装饰器函数可读性更好。

decorator.py

decorator.py 是一个非常简单的装饰器加强包。你可以很直观的先定义包装函数wrapper(),再使用decorate(func, wrapper)方法就可以完成一个装饰器。

from decorator import decorate 
 
def wrapper(func, *args, **kwargs): 
    """print log before a function.""" 
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) 
    return func(*args, **kwargs) 
 
def logging(func): 
    return decorate(func, wrapper)  # 用wrapper装饰func
登入後複製

你也可以使用它自带的@decorator装饰器来完成你的装饰器。

from decorator import decorator 
 
@decorator 
def logging(func, *args, **kwargs): 
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) 
    return func(*args, **kwargs)
登入後複製

decorator.py实现的装饰器能完整保留原函数的name,doc和args,唯一有问题的就是inspect.getsource(func)返回的还是装饰器的源代码,你需要改成inspect.getsource(func.__wrapped__)。

wrapt

wrapt是一个功能非常完善的包,用于实现各种你想到或者你没想到的装饰器。使用wrapt实现的装饰器你不需要担心之前inspect中遇到的所有问题,因为它都帮你处理了,甚至inspect.getsource(func)也准确无误。

import wrapt 
 
# without argument in decorator 
@wrapt.decorator 
def logging(wrapped, instance, args, kwargs):  # instance is must 
    print "[DEBUG]: enter {}()".format(wrapped.__name__) 
    return wrapped(*args, **kwargs) 
 
@logging 
def say(something): pass
登入後複製

使用wrapt你只需要定义一个装饰器函数,但是函数签名是固定的,必须是(wrapped, instance, args, kwargs),注意第二个参数instance是必须的,就算你不用它。当装饰器装饰在不同位置时它将得到不同的值,比如装饰在类实例方法时你可以拿到这个类实例。根据instance的值你能够更加灵活的调整你的装饰器。另外,args和kwargs也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。

如果你需要使用wrapt写一个带参数的装饰器,可以这样写。

def logging(level): 
    @wrapt.decorator 
    def wrapper(wrapped, instance, args, kwargs): 
        print "[{}]: enter {}()".format(level, wrapped.__name__) 
        return wrapped(*args, **kwargs) 
    return wrapper 
 
@logging(level="INFO") 
def do(work): pass
登入後複製

关于wrapt的使用,建议查阅官方文档,在此不在赘述。

http://wrapt.readthedocs.io/e...

小结

Python的裝飾器和Java的註解(Annotation)並不是同一回事,和C#中的特性(Attribute)也不一樣,完全是兩個概念。

裝飾器的理念是對原始函數、物件的加強,相當於重新封裝,所以一般裝飾器函數都被命名為wrapper(),意義在於包裝。函數只有在被呼叫時才會發揮其作用。例如@logging裝飾器可以在函數執行時額外輸出日誌,@cache裝飾過的函數可以快取計算結果等等。

而註解和特性則是對目標函數或物件添加一些屬性,相當於將其分類。這些屬性可以透過反射拿到,在程式運行時對不同的特性函數或物件加以乾預。例如有Setup的函數就當成準備步驟執行,或是找到所有有TestMethod的函數依序執行等等。

至此我所了解的裝飾器已經講完,但是還有一些內容沒有提到,例如裝飾類的裝飾器。有機會再補充。謝謝觀看。


本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡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靈活,廣泛用於前端和服務器端編程。

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系統以獲得更好的開發體驗和安全保障。

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

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

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

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

vscode怎麼在終端運行程序 vscode怎麼在終端運行程序 Apr 15, 2025 pm 06:42 PM

在 VS Code 中,可以通過以下步驟在終端運行程序:準備代碼和打開集成終端確保代碼目錄與終端工作目錄一致根據編程語言選擇運行命令(如 Python 的 python your_file_name.py)檢查是否成功運行並解決錯誤利用調試器提升調試效率

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

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

See all articles