Python装饰器

Python中的装饰器是一个用于修改类或者函数功能的可调用对象(callable),函数或者实现了__call__方法的类都可以看作是可调用对象。Python中装饰器分为两大类:

  • 函数装饰器

  • 类装饰器

函数装饰器

最简单的装饰器

Python中最简单的装饰器是一个嵌套函数。举例,使用装饰器函数elapsed来统计函数执行耗时:

# _*_ coding=utf8 _*_
import time
import logging
​
logging.basicConfig(level=logging.INFO)
​
​
def elapsed(func):
    """统计函数执行耗时"""
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        logging.info(f'exec {func.__name__} elapsed:{end_time-start_time}')
    return wrapper
​
​
def delay():
    logging.info('delay...')
    time.sleep(0.2)
​
​
# 装饰器就是普通函数,可以像普通函数那样直接调用
elapsed(delay)()
​

输出如下:

INFO:root:delay...
INFO:root:exec delay elapsed:0.20386290550231934

对于装饰器Python在语言层面给予了支持,对上面代码做如下修改:

@elapsed
def delay():
    logging.info('delay...')
    time.sleep(0.2)
​
​
delay()

输出入下:

INFO:root:delay...
INFO:root:exec delay elapsed:0.21143507957458496

使用装饰器后,打印函数信息,无法输出原函数信息,执行print(delay),输出<function elapsed.<locals>.wrapper at 0x0000018E8AFA4E50>

这里可以使用functools模块来解决:

import functools
​
def elapsed(func):
    """统计函数执行耗时"""
    @functools.wraps(func)
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        logging.info(f'exec {func.__name__} elapsed:{end_time-start_time}')
    return wrapper

再次执行print(delay),输出<function delay at 0x00000186651B4E50>

装饰器可以被多次使用:

@elapsed
@elapsed
def delay():
    logging.info('delay...')
    time.sleep(0.2)
​
​
delay()

输出如下:

INFO:root:delay...
INFO:root:exec delay elapsed:0.21126818656921387
INFO:root:exec wrapper elapsed:0.21126818656921387

上述代码等价于如下调用方式:

def delay():
    logging.info('delay...')
    time.sleep(0.2)
​
​
# 装饰器就是普通函数,可以像普通函数那样直接调用
# 这里多次调用装饰器函数
elapsed(elapsed(delay))()

调用堆栈如下:

"""
logging.info() # wrapper2
logging.info() # wrapper1
logging.info() # delay
delay()
wrapper1() # func=delay
wrapper2() # func=wrapper1
elapsed(wrapper1) # 返回新的wrapper,其中func指向wrapper1,这里记录为wrapper2
elapsed(delay) # 返回wrapper,其中func指向delay函数,这里记录为wrapper1
"""

若原函数需要传递参数,则需对装饰器做如下改造:

def elapsed(func):
    """统计函数执行耗时"""
    @functools.wraps(func)
    def wrapper(*args):
        start_time = time.time()
        func(*args)
        end_time = time.time()
        logging.info(f'exec {func.__name__} elapsed:{end_time-start_time}')
    return wrapper

带参数的装饰器

上节中提到的较为简单的装饰器是一个嵌套函数,带有参数的装饰器也是嵌套函数,只不过多嵌套一层:

# _*_ coding=utf8 _*_

import logging
import functools

logging.basicConfig(level=logging.DEBUG)


def exec(count):
    def exec_wrapper(func):
        @functools.wraps(func)
        def func_wrapper():
            for i in range(count):
                logging.info(f'exec {func}')
                func()
        return func_wrapper
    return exec_wrapper


@exec(3)
def func():
    pass


func()

输出如下:

INFO:root:exec <function func at 0x000001A783724E50>
INFO:root:exec <function func at 0x000001A783724E50>
INFO:root:exec <function func at 0x000001A783724E50>

上述示例中的装饰器同样无法直接给函数func传递参数,否则会抛异常:TypeError: func() takes 0 positional arguments but 1 was given,因为加上装饰器后,再调用func相当于调用func_wrapper函数。想要给原函数传递参数,需对装饰器做如下改造:

def exec(count):
    def exec_wrapper(func):
        @functools.wraps(func)
        def func_wrapper(*args, **kwargs):
            for i in range(count):
                logging.info(f'exec {func}')
                func(*args, **kwargs)
        return func_wrapper
    return exec_wrapper

func_wrapper函数中添加*args**kwargs两个参数,也可根据需要来添加其中某一个。

类装饰器

类装饰器与函数装饰器类似,只是类装饰器中要实现__call__方法:

class Elapsed:
    def __init__(self, func):
        self.__func = func

    def __call__(self, *args, **kwargs):
        start_time = time.time()
        self.__func(*args, **kwargs)
        end_time = time.time()
        logging.info(
            f'exec {self.__func.__name__} elapsed:{end_time-start_time}')
        
        
@Elapsed
def func(secs=0.1):
    time.sleep(secs)

装饰器不仅可以用于函数上,也可以用在类上,这里以类装饰器为例:

# _*_ coding=utf8 _*_

import logging
import time

logging.basicConfig(level=logging.DEBUG)


class LogClassName:
    def __init__(self, cls):
        self.__cls = cls

    def __call__(self, *args, **kwargs):
        logging.info(f'current class name: {self.__cls.__name__}')
        return self.__cls(*args, **kwargs)

    def __str__(self):
        return f'{self.__cls}'


@LogClassName
class Info:
    pass


logging.info(Info)
Info() 

异步装饰器

针对异步方法的装饰器需使用async/await来修饰,对elapsed装饰器进行异步改造:

def async_elapsed(name: None | str = None):
    def wrapper(func):
        @functools.wraps(func)
        async def executor(*args, **kwargs):
            start = DatetimeHelper.timestamp()
            await func(*args, **kwargs)
            end = DatetimeHelper.timestamp()
            __elapsed = end - start
            msg = f"函数({func})执行耗时:{__elapsed}ms" if not name else f"{name}执行耗时:{__elapsed}ms"
            print(msg)
​
        return executor
​
    return wrapper

 

Python中的装饰器与装饰器模式

Python中的装饰器和装饰器模式有着相同的目的:在不修改原有功能代码的基础上对其做扩展。

Python在语言层面对与装饰器给与了支持,相对比较简洁,经典的装饰器模式在编码实现上通常比Python装饰器有更多的代码量。Python装饰器要明确的作用域某个函数或类上,装饰器模式则是针对某种类型的方法做扩展,具体扩展的对象在运行时才确定。此外,装饰器模式可以作为面向对象中继承的替代。

二者有相同的目的,但实现方式不同,Python装饰器可以看作是静态扩展,装饰器模式是动态扩展。

推荐阅读

What is the difference between Python decorators and the decorator pattern?

 

posted @ 2021-12-05 17:54  雪飞鸿  阅读(197)  评论(0编辑  收藏  举报