Python 的装饰符

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