一文看懂 Python 的装饰器

目录

在Python中使用装饰器可以在不修改代码的前提下为已有的函数添加新功能,例如打印日志、缓存数据等。

为什么需要装饰器

假如你需要为某个函数添加一个新功能。直接的办法是在该函数中实现这个功能,或者在新的函数中实现它,然后在该函数中调用新函数。

间接的办法是执行新的函数,新函数实现了该功能,并调用原来的函数。装饰器便是这样的函数。

既然直接的办法能解决问题,为什么还需用装饰器?

在实际中,随着业务的变化,项目会越来越复杂,比如:

  1. 类之间重复的代码越来越多。

  2. 项目代码包含大量与业务无关的功能,例如调试、缓存、鉴权等。

为了解决上述问题,第一个办法是把重复的代码和业务无关的功能抽象出来,然后在新的类中实现。这样一来,新类与旧类在项目中耦合,新类的改变会影响到旧类,项目的维护成本增加。

第二个思路是,在旧类需要的时候(编译或运行时)动态地加入这些功能,即 面向切面编程。切面代表了一个功能点,站在业务的角度,它是与业务逻辑无关的功能。

面向对象的思想是把业务逻辑划分成不同的类,而面向切面为业务逻辑提供补充功能。前者是纵向切分,后者是横行切分。一纵一横,保持项目代码简洁。

装饰器实现的就是切面功能点。下文用一个例子说明三种类型点装饰器:

  • 普通装饰器
  • 带参数的装饰器
  • 装饰器类

普通装饰器

我们即将实现一个 计时器 timer,它的功能是计算函数的运行时间。使用效果如下所示:

# main.py
import time

@timer
def test():
	time.sleep(1.0)

if __name__ == '__main__':
	test()
time cost: 1.0013980865478516

装饰器 timer 的输入参数是一个函数对象,返回的结果也是一个函数对象。返回的函数是一个 附加了新功能的函数

在上面的例子中, 装饰器 @timer 实际上是一个执行如下操作的语法糖。

wrapper_func = timer(test)
wrapper_func()

完整代码如下所示。

import time


# 装饰器函数
# 输入是一个函数, 返回一个装饰过的函数wrapper
def timer(func):
	
	# 调用func并计时
    def wrapper(*args, **kwargs):
        t1 = time.time()
        func(*args, **kwargs)
        print("time cost:", time.time() - t1)

    return wrapper

@timer
def test():
	time.sleep(1.0)

if __name__ == '__main__':
	test()

装饰后的函数名

在上述例子中,如果我们打印使用装饰器之后的函数 test 的名称,结果是 wrapper

>>> print(test.__name__)
wrapper

在一些情况下,我们希望保持原来函数的名称,这时可以利用Python自带的装饰器 @wraps(func) 来装饰 wrapper,从而保持函数名称不变(代码如下)。

import time
from functools import wraps


# 装饰器函数
# 输入是一个函数, 返回一个装饰过的函数wrapper
def timer(func):
	
	# 调用func并计时
	@wraps(func)  # 保持func的函数名
    def wrapper(*args, **kwargs):
        t1 = time.time()
        func(*args, **kwargs)
        print("time cost:", time.time() - t1)

    return wrapper

带参数的装饰器

下面我们要把计时器的功能稍微扩充一下。实现一个带参数的计时器@timer_unit(unit),其中参数 unit 代表了计时的单位: 's' -- 秒; 'ms' -- 毫秒

前面提到,装饰器函数的输入必须是一个函数对象。注意到函数名加括号 timer_unit(unit='s') 代表执行一个函数,因此只要让函数 timer_unit(unit='s') 的执行结果返回一个普通的装饰器即可。

# 带参数的装饰器
def timer_unit(unit='s'):
	
	# 普通装饰器
    def decorator(func):
		
		# 调用func并计时
		@wraps(func)  # 保持func的函数名
        def wrapper(*args, **kwargs):
            t1 = time.time()
            func(*args, **kwargs)
            multiplier = 1
            if unit == 'ms':
                multiplier = 1000
            print("time cost (unit: %s):" % unit, (time.time() - t1) * multiplier)

        return wrapper
        
	# 返回普通装饰器
    return decorator

装饰器类

我们还要给上述装饰器增加新的计时单位 'mus: 微秒'。直接的方法是在 wrapper 函数中增加相应的逻辑。

这样做的缺点是,随着装饰器功越来越复杂,如果所有的逻辑在一个函数里实现,不利于协作和维护。这时可以用类的方式来实现装饰器的功能。

import time
from functools import wraps


class Timer(object):
    """ 一个用来计时的装饰器类
    需要实现__call__方法, 可以像函数一样调用实例
    """

    def __init__(self):
        self._multiplier = 1  # 秒
        self._unit = None  # 's'-秒; 'ms'-毫秒; 'mus' - 微秒

    def _get_multiplier(self):
        if self._unit == 'ms':  # 毫秒
            self._multiplier = 1000
        if self._unit == 'mus':  # 微秒
            self._multiplier = 1000000
        return self._multiplier

    def __call__(self, unit='s'):

        self._unit = unit

        def decorator(func):
        
        	@wraps(func)
            def wrapper(*args, **kwargs):
                t1 = time.time()
                func(*args, **kwargs)
                print("time cost (unit: %s):" % unit, (time.time() - t1) * self._get_multiplier())

            return wrapper

        return decorator


# 装饰器函数
timer_class = Timer()


@timer_class(unit='mus')
def test():
    time.sleep(1)


if __name__ == '__main__':
    test()

最后说一句,不要滥用装饰器。

标签 :

相关文章

用 Prophet 做时间序列预测

电商的业务场景中有很多决策依赖预测模型的输入,其中时间序列预测是一类比较基础的模型,服务于采购、营销、仓配、客服等业务。本篇介绍开源时序预测框架Prophet的基本原理和使用方法。

更多