Python装饰器语法进阶

Python装饰器入门与简单应用中,我们学习了了解Python中的装饰器所需的几个必备知识,进而初步探究了最简单的一类装饰器及其应用:装饰器和被装饰对象都是函数。本文将带你了解有关装饰器的几个进阶语法:

  • 如何使用函数形式装饰器来装饰一个类;
  • 多个装饰器同时装饰一个对象的效果如何;
  • 如何定义一个可以接收也可以不接收参数的装饰器;
    • 如何以类的形式定义一个装饰器,进而装饰一个函数或另一个类。

一、装饰器深入探究

1. 装饰器装饰一个类

一般来说,对类使用装饰器有两种方式,即:

1.1 装饰类中方法

第一种方式是装饰在一个类中定义的方法,这也是Python当初引入装饰器的一个原因。如:在Python装饰器入门与简单应用中,我们定义了@debug@timer两个装饰器,下面的代码显示这两个装饰器也可用于装饰自定义类中的方法:

from decorators import debug, timer


class TimeWaster:
    @debug  # 1
    def __init__(self, max_num):
        self.max_num = max_num

    @timer  # 2
    def waste_time(self, num_times):
        for each in range(num_times):
            sum([i ** 2 for i in range(self.max_num)])


def main():
    tw = TimeWaster(1000)  # 3

    tw.waste_time(999)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

Calling __init__(<__main__.TimeWaster object at 0x7f1c123444a8>, 1000)
‘__init__’ returned None
Finished ‘waste_time’ in 0.1853 secs

结合上述代码和其运行结果,有几点需要说明的是:

  • # 1 处的@debug相当于__init__ = debug(__init__),又因为函数debug()的返回值为wrapper_debug,故进一步有__init__ = wrapper_debug
  • 由上述结论,则# 3 处创建对象时调用初始化方法__init__(self, max_num)的代码相当于wrapper_debug(self, max_num)
  • # 2处的@timer相当于waste_time = timer(waste_timer),又因为timer()函数的返回值为wrapper_timer,故进一步有waste_time = wrapper_timer

实际上,Python中內置了一些常用的此类装饰器,如:@classmethod@staticmethod以及@property。其中,@classmethod@staticmethod用于在类的内部定义不与该类任何特定实例相绑定的方法;而@property装饰器用于自定义访问实例属性的gettersetter方法。

1.2 装饰整个类

第二种对类使用装饰器的方式为装饰器整个类。实际上,用于装饰一个类的装饰器和用于函数的差别不大,主要区别仅在于此时装饰器接收一个类而非一个函数作为参数,基于这一点,在Python装饰器入门与简单应用中定义的装饰器都可用于装饰一个类。如:下面代码还是使用@timer装饰器,来装饰一个类:

from decorators import timer


@timer  # 1
class TimeWaster(object):
    def __init__(self, max_num):
        self.max_num = max_num

    def waste_time(self, num_times):
        for each in range(num_times):
            sum([i ** 2 for i in range(self.max_num)])


def main():
    tw = TimeWaster(1000)  # 1
    tw.waste_time(999)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

Finished ‘TimeWaster’ in 0.0000 secs

为了理解上述代码及其运行结果,只要想着此处:

  • 因为@timer即等价于TimeWaster = timer(TimeWaster),且函数timer()的返回值为wrapper_timer
  • 所以# 1处代码相当于tw = wrapper_timer(1000)
  • 因此,结合timer()函数内部代码,可知上述代码等价于测量创建并初始化TimeWaster对象的时间,并将对应引用赋给tw变量。

2. 多个装饰器同时使用

在Python,多个装饰器可对同一个函数进行装饰,如下列代码所示:

def star(func):
    def wrapper_star(*args, **kwargs):
        print("*" * 30)
        func(*args, **kwargs)
        print("*" * 30)

    return wrapper_star


def percent(func):
    def wrapper_percent(*args, **kwargs):
        print("%" * 30)
        func(*args, **kwargs)
        print("%" * 30)

    return wrapper_percent


@star
@percent
def printer(msg):
    print(msg)


def main():
    printer("Hello")  # 1


if __name__ == '__main__':
    main()

上述代码的运行结果为:

******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************

实际上,产生上述运行结果的原因在于,上述代码中:

@star
@percent
def printer(msg):
    print(msg)

等价于:

def printer(msg):
    print(msg)
printer = star(percent(printer))

具体地:

  • star(percent(printer))意味着此时percent(printer)作为参数func传入了star()函数,且star()的返回值wrapper_star被赋给了printer变量;
  • 执行# 1处代码等价于执行wrapper_star("Hello"),故程序会先打印出30个*,而后执行func(*args, **kwargs)等价于执行percent(printer)("Hello")
  • 此时printer被当作参数func传入percent()函数,又percent()函数的返回值为wrapper_percent,则percent(printer)("Hello")等价于wrapper_percent("Hello")
  • 故程序又会打印30个%,而后func(*args, **kwargs)打印出Hello,接着程序又打印30个%
  • 最终,wrapper_star()函数中的func(*args, **kwargs)调用返回,程序打印出最后30个*

3. 装饰器接收参数

假设现在需要实现这样一个简单的需求:定义一个名为@repeat的装饰器,要求其可以接收一个指定次数的参数num_times,使得可以让被装饰函数在执行时会被重复调用num_times次,如下列代码:

@repeat(num_times=4)
def greet(name):
    print(f"Hello {name}")
    

def main():
    greet("Eric Idle")
    

if __name__ == '__main__':
    main()

即希望该代码的输出结果为:

Hello Eric Idle
Hello Eric Idle
Hello Eric Idle
Hello Eric Idle

那么,应该如何实现这一需求呢?

实际上,只要使得repeat(num_times=4)是一个函数调用,且其返回值指向一个装饰器,那么将该装饰器名称置于@符号之后,用来装饰greet()函数即可,此时在repeat()函数内部定义的装饰器函数和变量num_times就形成了一个闭包

基于上述分析,可以有如下接收参数的装饰器代码:

import functools


def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for each in range(num_times):
                value = func(*args, **kwargs)

            return value

        return wrapper_repeat

    return decorator_repeat

因此,在执行@repeat(num_times=4)时,实际上分成了两步:

  • 第一步:先执行repeat(num_times=4),此时其返回值为decorator_repeat
  • 第二步:执行装饰器@decorator_repeat

4. 可不接受参数装饰器

实际上,对于上述接受参数的装饰器,通过一定的修改,可得到一个既可以接受也可以不接受参数的装饰器,从而使得代码变得更加灵活。

如前所述,因为当一个装饰器接受参数时,你只需要在装饰器函数外再加一个外层函数即可,所以要实现可不接受参数的装饰器,只需要在新加的外层函数内判断装饰器在被调用时是否传参了即可。

具体地,由于待装饰函数只有在应用装饰器时未传递参数才会自动被当作参数传入最外层函数。这意味着对最外层函数而言,此时在其通过位置参数接收待装饰函数的地方应当为可选传递,即为关键字参数且默认为None,因此对上述@repeat装饰器修改如下:

import functools


def repeat(_func=None, num_times=1):  # 1
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value

        return wrapper_repeat

    if _func is None:
        return decorator_repeat
    else:
        return decorator_repeat(_func)

当执行以下代码时:

@repeat
def say_whee():
    print("Whee!")

由于@repeat相当于say_whee = repeat(say_whee),此时say_whee赋给_func不为None,故此时repeat(say_whee)的返回值为decorator_repeat(say_whee),即此时say_whee = decorator_repeat(say_whee),这根据Python装饰器的语法糖含义即等价于@decorator_repeat

当再执行下列代码时:

@repeat(num_times=3)
def greet(name):
    print(f"Hello {name}")

由于@repeat(num_times=3)可理解为两步:

  • 调用repeat(num_times=3):由于此时_func取默认值None,则repeat(num_times=3)的返回值为decorator_repeat
  • decorator_repeat置于@之后来装饰函数greet()

5. 类作为装饰器

假设现在有这样一个需求:定义一个装饰器,使之可以记录被装饰函数的调用次数。自然地,为实现该需求,很容易想到需要在装饰器中定义一个用于存储次数的属性。进一步地,可以联想到在面向对象的概念中,属性通常和由类创建的实例对象紧密相关,于是考虑该装饰器为一个类。

首先,需要再次明确的是,Python中的语法@decorator只是一个语法糖,其等价于func = decorator(func),因此,如果decorator是一个类的话,其__init__方法需要接受func作为参数。

其次,由于执行@decorator等价于func = decorator(func),且decorator为一个类,此时func不再指向一个函数,而是指向一个对象,此时要想通过func()的方式对其进行调用,根据必备四:Python可调用对象本质,类decorator还需要实现__call__()方法。

因此,通常实现一个可作为装饰器的类一般至少需要实现__init__()__call__()两个方法。

基于上述讨论,可实现需求的装饰器代码及其应用如下:

import functools


class CountCalls(object):
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__!r}")
        return self.func(*args, **kwargs)


@CountCalls
def say_whee():
    print("say_whee")


def main():
    say_whee()
    say_whee()


if __name__ == '__main__':
    main()

上述代码的运行结果为:

Call 1 of say_whee
say_whee
Call 2 of say_whee
say_whee

需要再次说明的是:

  • __init__()方法必须保存指向被装饰函数的引用,除此之外,该方法还可以完成其他任何与初始化相关的任务;
  • say_whee()函数在被装饰完成后,在执行say_whee()时,实际上被调用的是__call__()方法,该方法实际上类似于前面例子中的wrapper()函数;
  • 为保证被装饰函数的自省特性,此处使用了functools包中的update_wrapper()函数而非装饰器@functools.wraps

二、参考资料

猜你喜欢

转载自blog.csdn.net/weixin_37780776/article/details/106482351