關於 Python 裝飾器,你應該知道的知識

 

Python 裝飾器是一個強大的概念,允許我們使用一個函數 「 包裝 」 另一個函數

除了正常的職責之外,裝飾器的另類使用想法是抽象出你想要一個功能或類做的東西,這可能有很多原因,例如 代碼重用 和堅持 科裏原則

通過學習如何編寫自己的裝飾器,我們可以顯着提高自己代碼的可讀性,因爲它們可以更改函數的行爲方式,而無需實際更改代碼 ( 例如添加日誌記錄行 )

它們是 Python 中相當常用的工具,對於使用諸如 flask 或click 之類的框架的人來說很熟悉

雖然很多人只知道如何使用它們,而不知道如何編寫自己的裝飾器

這篇文章是由朋友 @Timber 帶給我們的客串,如果你有興趣爲我們寫作,請隨時在 Twitter 上與我們聯繫

它 ( 裝飾器 ) 怎樣工作 ?

首先,讓我們在 Python 中展示一個裝飾器的例子,這是一個非常基本的裝飾器的例子

@my_decorator
def hello():
    print('hello')

當我們在 Python 中定義函數時,該函數將成爲一個對象,也就是說,Python 中,任何函數都是一個對象,可調用的對象

上面的函數 hello 是一個函數對象,@my_decorator 實際上是一個能夠使用 hello 對象並將另一個對象返回給解釋器的函數

裝飾器返回的對象就是所謂的 hello 。從本質上講,它就像你要編寫自己的普通函數一樣,例如 hello = decorate ( hello )

裝飾可以接收一個函數作爲參數 - 它可以使用任何它想要的 - 然後返回另一個對象

如果需要,裝飾器可以吞下函數 ( 也就是不返回該函數 ) ,或返回不是函數的函數

編寫自己的裝飾器

如上所述,裝飾器只是一個傳遞函數的函數,並返回一個對象

所以,要開始編寫裝飾器,我們只需要定義一個函數

def my_decorator(f):
    return 5

任何函數都可以用作裝飾器。在這個例子中,裝飾器接收一個函數,並返回一個不同的對象。它只是完全吞下傳遞給它的函數,並且總是返回 5

@my_decorator
def hello():
    print('hello')
>>> hello()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
'int' object is not callable

因爲我們的裝飾器返回一個 int 而不是一個可調用的,所以它不能作爲函數調用

請記住,裝飾器的返回值替換了 hello

>>> hello
5

在大多數情況下,我們希望裝飾器返回的對象實際上模仿我們裝飾的函數。這意味着裝飾器返回的對象本身需要是一個函數

例如,假設我們只想在每次調用函數時打印,我們可以編寫一個打印該信息的函數,然後調用該函數。但是該函數需要由裝飾器返回

這通常會導致函數嵌套,例如

def mydecorator(f):  # f is the function passed to us from python
    def log_f_as_called():
        print(f'{f} was called.')
        f()
    return log_f_as_called

正如你所見,我們定義了一個嵌套的函數,而裝飾器函數則返回剛剛定義的嵌套函數。這樣,函數 hello 仍然可以像標準函數一樣被調用,調用者不需要知道它是否被裝飾

我們現在可以將 hello 定義如下

@mydecorator
def hello():
    print('hello')

我們會得到如下的輸出

>>> hello()
<function hello at 0x7f27738d7510> was called.
hello

注意:<function hello at 0x7f27738d7510> 引用內的數字對每個運行來說都不同,它代表內存地址

正確包裝函數

如果需要,可以多次裝飾一個函數。這種情況下,裝飾器會產生鏈式效應。基本上,頂部裝飾器從前者傳遞對象,依此類推。例如,如果我們有以下代碼

@a
@b
@c
def hello():
    print('hello')

解釋器本質上是執行 hello = a(b(c(hello))) 並且所有裝飾器將相互包裝

您可以使用我們現有的裝飾器自己測試,並使用它兩次

@mydecorator
@mydecorator
def hello():
    print('hello')

>>> hello()
<function mydec.<locals>.a at 0x7f277383d378> was called.
<function hello at 0x7f2772f78ae8> was called.
hello

您將注意到第一個裝飾器,包裹第二個裝飾器,並單獨打印

你可能注意到這裏的一個有趣的事情是,第一行打印了 <function mydec.<locals>.a at 0x7f277383d378> 而不是第二行打印,而我們所期待的是:<function hello at 0x7f2772f78ae8>.

這是因爲裝飾器返回的對象是一個新函數,而不是 hello 。這對於我們這個簡單的例子來說很好,但是經常會破壞可能試圖反省函數屬性的測試和事情

如果你的想法是裝飾器像它裝飾的函數一樣,它還需要模仿該函數。幸運的是,Python 標準庫 functools 模塊中有一個名爲 wraps 的裝飾器

import functools
def mydecorator(f): 
    @functools.wraps(f)  # we tell wraps that the function we are wrapping is f
    def log_f_as_called():
        print(f'{f} was called.')
        f()
    return log_f_as_called

@mydecorator
@mydecorator
def hello():
    print('hello')

>>> hello()
<function hello at 0x7f27737c7950> was called.
<function hello at 0x7f27737c7f28> was called.
hello

現在,我們的新函數就像它的包裝/裝飾一樣。但是,我們仍然依賴於它什麼都不返回,並且不接受任何輸入的事實

如果我們想要更通用,我們需要傳入參數並返回相同的值。我們可以修改我們的函數讓它看起來像這樣

import functools
def mydecorator(f): 
    @functools.wraps(f)  # wraps is a decorator that tells our function to act like f
    def log_f_as_called(*args, **kwargs):
        print(f'{f} was called with arguments={args} and kwargs={kwargs}')
        value = f(*args, **kwargs)
        print(f'{f} return value {value}')
        return value
    return log_f_as_called

現在我們每次調用函數時都會打印,包括函數接收的所有輸入以及返回的內容。現在,你可以簡單地裝飾任何現有函數,並在其所有輸入和輸出上進行調試日誌記錄,而無需手動編寫日誌記錄代碼

給裝飾器添加變量

如果我們使用裝飾器來處理我們想要發佈的任何代碼,而不僅僅是本地代碼,那麼可能希望用 logging語句替換所有 print 語句。這種情況下,我們需要定義日誌級別。假設我們默認使用 debug 日誌級別,但這也可能取決於函數

我們可以爲裝飾器本身提供變量,以定義它應該如何表現。例如

@debug(level='info')
def hello():
    print('hello')

上面的代碼將允許我們指定此特定函數應該在 info 級別而不是 debug 級別進行日誌記錄。這在 Python 中中是通過編寫一個返回裝飾器的函數實現的

是的,裝飾者也是一個函數。所以這基本上是說 hello = debug('info')(hello)。這個雙括號可能看起來很時髦,但基本上,debug 是函數,它返回一個函數

爲了將它添加到我們現有的裝飾器中,我們需要再嵌套一次,現在使我們的代碼看起來如下所示

import functools
def debug(level): 
    def mydecorator(f)
        @functools.wraps(f)
        def log_f_as_called(*args, **kwargs):
            logger.log(level, f'{f} was called with arguments={args} and kwargs={kwargs}')
            value = f(*args, **kwargs)
            logger.log(level, f'{f} return value {value}')
            return value
        return log_f_as_called
    return mydecorator

上面的更改將 debug 變爲一個函數,該函數返回一個使用正確日誌記錄級別的裝飾器,這變得有點難看,並且過度嵌套

我想做一些小技巧來解決這個問題,就是添加一個 kwarg 參數 level 並且默認只爲 debug 並返回一個 partial

partial 是一個 「 非完整函數調用 」,它包含一個函數和一些參數,因此它們作爲一個對象傳遞而不實際調用該函數

import functools
def debug(f=None, *, level='debug'): 
    if f is None:
        return functools.partial(debug, level=level)
    @functools.wraps(f)   # we tell wraps that the function we are wrapping is f
    def log_f_as_called(*args, **kwargs):
        logger.log(level, f'{f} was called with arguments={args} and kwargs={kwargs}')
        value = f(*args, **kwargs)
        logger.log(level, f'{f} return value {value}')
        return value
    return log_f_as_called

現在裝飾器可以正常工作

@debug
def hello():
    print('hello')

然後就可以使用 debug 級別記錄日誌,或者,覆蓋日誌級別

@debug('warning')
def hello():
    print('hello')
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章