Python 的裝飾符

Jason990420發表於2020-06-11

Python 的裝飾符

上回我發了一篇文章談到 functools 模組, 這回就來談談 Python 的裝飾符 ‘@’.

裝飾函式

這裡我們先說明裝飾函式, 針對某些函式我們想要在呼叫前和呼叫後作一些動作, 就必須加寫一些語句來處理. 比如我們想要記錄一個函式被呼叫的時間以及函式執行所發的時間.

>>> from datetime import datetime
>>>
>>> def circle_area(radius):
>>>     area = 3.14*radius**2
>>>     print(f'Area of circle with radius {radius} is {area}')
>>>     return area
>>>
>>> now = datetime.now()
>>> print(f'func called at {now}')
>>> circle_area(3)
>>> print(f'Execution time is {datetime.now()-now}')
func called at 2020-06-11 20:33:37.828129
Area of circle with radius 3 is 28.26
Execution time is 0:00:00.007016

當然這樣可以達到我們要的結果, 但是如果有別的函式也要作同樣的事呢? 那不是又要增加那些程式碼? 函式是用來處理這樣問題. 因此我們就要寫一個函式專門作這事.

>>> from datetime import datetime
>>>
>>> def record_time(func, *args, **kwargs):
>>>     now = datetime.now()
>>>     print(f'func called at {now}')
>>>     value = func(*args, **kwargs)
>>>     print(f'Execution time is {datetime.now()-now}')
>>>     return value
>>>
>>> def circle_area(radius):
>>>     area = 3.14*radius**2
>>>     print(f'Area of circle with radius {radius} is {area}')
>>>     return area
>>>
>>> record_time(circle_area, 3)
func called at 2020-06-11 20:40:32.256889
Area of circle with radius 3 is 28.26
Execution time is 0:00:00.008006

這樣就可以在各個需要的函式來呼叫 record_time, 這個 record_time 就可以稱作裝飾函式.

裝飾符

使用裝飾函式, 還是有點複雜, 因為呼叫各個函式時, 都得加上裝飾函式, 還要分割引數. 因此在 Python 中就有一個所謂的裝飾符 ‘@’, 可以來幫我們簡化這件事. 基本上, 就是建立一個新的函式, 來取代你想用的函式, 其中新的函式前後包含你想要作的事.

from datetime import datetime

def record_time(func):
    """ 建立一個新的函式 """
    def new_function(*args, **kwargs):
        now = datetime.now()
        print(f'func called at {now}')
        value = func(*args, **kwargs)
        print(f'Execution time is {datetime.now()-now}')
        return value
    """ 返回這個新的函式 """
    return new_function

""" 裝飾符的使用 """
@record_time
def circle_area(radius):
    area = 3.14*radius**2
    print(f'Area of circle with radius {radius} is {area}')
    return area

circle_area(3)

該裝飾符就放在函式定義前一行, 其意義等同於把 circle_area 函式更改為我們在 record_time 中所定義的新函式.

def circle_area(radius):
    area = 3.14*radius**2
    print(f'Area of circle with radius {radius} is {area}')
    return area

""" circle_area 被取代為 record_time 中定義的 decorator """
circle_area = record_time(circle_area)

所以我們表面上用的是 circle_area, 事實上, 它已經被改成我們所定義的另一個新函式.

裝飾函式的引數

裝飾符的使用有兩種方式, 類似 function 代表函式本身, 而 function() 代表函式呼叫後的結果. 以下例為例子:

@record_time 表示提供一個函式 record_time 來作包裝, 等同
    circle_area1 = record_time(circle_area1) 也就是 wrapper

@record_time(n=2) 表示呼叫 record_time(n=2) 的結果來取代原函式, 等同
    circle_area2 = record_time(n=2)(circle_area2)
>>> from datetime import datetime
>>>
>>> def record_time(_func=None, n=1):
>>>     """ n 代表重複的次數 """
>>>     def decorator(func):
>>>         def wrapper(*args, **kwargs):
>>>             now = datetime.now()
>>>             print(f'{func.__name__} called at {now}')
>>>             for i in range(n):
>>>                 value = func(*args, **kwargs)
>>>             print(f'Execution time is {datetime.now()-now}')
>>>             return value
>>>         return wrapper
>>>     if _func is None:
>>>         return decorator
>>>     else:
>>>         return decorator(_func)
>>>
>>> @record_time
>>> def circle_area1(radius):
>>>     area = 3.14*radius**2
>>>     print(f'Area of circle with radius {radius} is {area}')
>>>     return area
>>>
>>> @record_time(n=2)
>>> def circle_area2(radius):
>>>     area = 3.14*radius**2
>>>     print(f'Area of circle with radius {radius} is {area}')
>>>     return area
>>>
>>> circle_area1(3)
>>> circle_area2(3)

circle_area1 called at 2020-06-12 09:50:52.705339
Area of circle with radius 3 is 28.26
Execution time is 0:00:00.010033
circle_area2 called at 2020-06-12 09:50:52.718341
Area of circle with radius 3 is 28.26
Area of circle with radius 3 is 28.26
Execution time is 0:00:00.019950


裝飾函式的屬性

裝飾函式可以加上屬性, 用來記錄狀態. 如下例可以記錄函式被呼叫的次數.
在本例子中, 如果裝飾的函式不同, 其呼叫的次數都會被累加在一起. 如果你想區分不同函式被呼叫的次數, 可以自建一個以 function.__name__:count_value 為鍵值對的字典分開統計.

>>> def times(func):
>>>     def decorator(*args, **kwagrs):
>>>         times.count += 1
>>>         print(f'This function called {times.count} times.')
>>>         value = func(*args, **kwagrs)
>>>         return value
>>>     times.count = 0
>>>     return decorator
>>>
>>> @times
>>> def circle_area(radius):
>>>     area = 3.14*radius**2
>>>     print(f'Area of circle with radius {radius} is {area}')
>>>     return area
>>>
>>> circle_area(3)
>>> circle_area(4)
>>> circle_area(5)

This function called 1 times.
Area of circle with radius 3 is 28.26
This function called 2 times.
Area of circle with radius 4 is 50.24
This function called 3 times.
Area of circle with radius 5 is 78.5


裝飾符的疊加使用

疊加使用裝飾符, 以下例

>>> @decorator_third
>>> @decorator_second
>>> @decorator_first
>>> def circle_area(radius):
>>>     area = 3.14*radius**2
>>>     print(f'Area of circle with radius {radius} is {area}')
>>>     return area

其結果如同

circle_area = decorator_third(decorator_second(decorator_first(circle_area)))


類的裝飾符

類的裝飾符建立方式有兩種, 一種類似函式的使用, 另一種為了可以呼叫, 必須建立一個 __call__ 函式, 這裡就不細說了, 有興趣的再自行看一下 Python 的檔案說明.

函式的代表符

旣然函式都被取代了, 你看到的函式代表符就不再是原函式的名稱

>>> circle_area
<function record_time.<locals>.new_function at 0x000000000742E9D0>

那怎麼辦呢, 一樣, 再加個裝飾符, 當然你可以自己設計, 也可以使用現成的@functools.wraps(func)

from datetime import datetime
import functools

def record_time(func):
    """ 新增裝飾符 """
    @functools.wraps(func) 
    def new_function(*args, **kwargs):
        now = datetime.now()
        print(f'func called at {now}')
        value = func(*args, **kwargs)
        print(f'Execution time is {datetime.now()-now}')
        return value
    return new_function

@record_time
def circle_area(radius):
    area = 3.14*radius**2
    print(f'Area of​​ circle with radius {radius} is {area}')
    return area

circle_area(3)
>>> circle_area
<function circle_area at 0x00000000082EE940>
本作品採用《CC 協議》,轉載必須註明作者和本文連結
Jason Yang

相關文章