Python装饰器进阶语法学以致用

Python装饰器语法进阶中,我们学习了Python装饰器的进阶语法,那么这些看上去花里胡哨的语法究竟有何用途,如何才能加深对这些语法的理解并应用于实际的代码中,这是本文即将要探讨的问题。

一、应用案例

1. 再议为代码减速

Python装饰器入门与简单应用代码减速案例中,装饰器@slow_down总是让被装饰函数休眠一秒,而在Python装饰器语法进阶中我们知道了如何为装饰器添加参数,因此下面重写@slow_down代码,使之可以接收一个可选参数rate,该参数用以指定被装饰函数countdown()的休眠时间长度:

import functools
import time


def slow_down(_func=None, rate=1):
    """使得程序在被调用之前休眠由rate指定的时间"""

    def decorator_slow_down(func):
        @functools.wraps(func)
        def wrapper_slow_down(*args, **kwargs):
            time.sleep(rate)
            return func(*args, **kwargs)

        return wrapper_slow_down

    if _func is None:
        return decorator_slow_down
    else:
        return decorator_slow_down(_func)  # 1


@slow_down(rate=2)
def countdown(from_number):
    if from_number < 1:
        print("发射!")
    else:
        print(from_number)
        countdown(from_number - 1)


def main():
    countdown(5)


if __name__ == '__main__':
    main()

对于上述代码,需要说明的是,当不为装饰器传参数,即使用@slow_down直接装饰函数时,此语法等价于countdown = slow_down(countdown),则被装饰函数的引用会被传递至_func处,此时_func不为None,则slow_down()函数会在# 1处返回,此时# 1相当于return wrapper_slow_down,即使用@slow_down和使用@decorator_slow_down实现的效果一样。

至于为什么不能直接在# 1处写成return wrapper_slow_down,原因在于:当使用@slow_down执行装饰操作时,wrapper_slow_down()函数因为在最内部还未被定义,故此时编译器无法找到该变量。

2. 创建单例的对象

所谓单例是指这样一个类,该类只有一个实例,即无论使用该类创建多少次实例对象,返回的都是同一个实例。

实际上,在Python中你经常使用几个单例,如:NoneTrue以及False,同时也因为None是一个单例,所以你才可以使用is关键字来比较None和另外一个对象,而只有当待比较的两个对象是统一个类的实例时,is关键字的返回值才为True

下面的@singleton装饰器就将一个类变成了一个单例,其实现方式为:

  • 将该类的第一个实例保存为内层函数对象wrapper_singleton的一个属性;
  • 后续创建实例时都返回上述属性中保存的实例的引用。
import functools


def singleton(cls):
    @functools.wraps(cls)
    def wrapper_singleton(*args, **kwargs):
        if not wrapper_singleton.instance:
            wrapper_singleton.instance = cls(*args, **kwargs)  # 4
        return wrapper_singleton.instance

    wrapper_singleton.instance = None  # 1
    print("-" * 30)  # 2
    return wrapper_singleton  # 3


@singleton  # TheOne = singleton(TheOne) = wrapper_singleton
class TheOne(object):
    pass


def main():
    first_one = TheOne()
    second_one = TheOne()

    print(first_one is second_one)
    print("id(first_one) = ", id(first_one))
    print("id(second_one) = ", id(second_one))


if __name__ == '__main__':
    main()

上述代码的运行结果为:

------------------------------
first_one is second_one = True
id(first_one) = 139627414325064
id(second_one) = 139627414325064

由上述运行结果可知,@singleton装饰器的确将类TheOne变成了一个单例。

为了更好的理解上述代码,有以下几点需要说明:

  • 执行@singleton时,# 1# 2# 3处的代码立即被执行,这也是为什么上述运行结果会先打印出30个-符号;
  • 当通过TheOne()的方式创建实例对象时,由于装饰器的作用,此时TheOne()等价于wrapper_singleton()
    • 当第一次创建实例时,由于if条件判断为真,则创建实例,且该实例的引用赋给了wrapper_singleton.instance
    • 当后续再创建实例时,由于if条件判断为假,则直接返回属性wrapper_singleton.instance指向的实例对象引用。

3. 缓存函数返回值

装饰器可以提供优秀的缓存机制。为进行具体阐述,下面代码先通过递归方式定义了一个斐波那契数列函数,并使用类CountCalls作为装饰器统计求取指定序号数列的值时斐波那契数列函数被调用的次数:

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  # fibonacci = CountCalls(fibonacci)
def fibonacci(num):
    if num < 2:
        return num  # 当num = 0时,fibonacci(0) = 0;当num = 1时,fibonacci(1) = 1;当num = 2时,fibonacci(2) = 1
    return fibonacci(num - 1) + fibonacci(num - 2)


def main():
    print(fibonacci(5))
    

if __name__ == '__main__':
    main()

上述代码的运行结果为:

Call 1 of ‘fibonacci’
Call 2 of ‘fibonacci’
Call 3 of ‘fibonacci’
Call 4 of ‘fibonacci’
Call 5 of ‘fibonacci’
Call 6 of ‘fibonacci’
Call 7 of ‘fibonacci’
Call 8 of ‘fibonacci’
Call 9 of ‘fibonacci’
Call 10 of ‘fibonacci’
Call 11 of ‘fibonacci’
Call 12 of ‘fibonacci’
Call 13 of ‘fibonacci’
Call 14 of ‘fibonacci’
Call 15 of ‘fibonacci’
5

即为了计算斐波那契数列的第5项,上述fibonacci()函数被调用了15次,当计算第10项时,调用次数到了177,再当计算第15项时,调用次数竟然增加至了1973次,造成如此快速地次数增加,是因为上述代码每次都会重新递归计算所有前序项,如下图所示:
在这里插入图片描述

下面通过对前序计算得出的数列项进行缓存来改善上面代码的弊端:

import functools


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

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


def cache(func):  # 2.2 此时func = CountCalls(fibonacci)
	"""缓存已计算过的斐波那契数列项的值"""

	@functools.wraps(func)
	def wrapper_cache(*args, **kwargs):
		cache_key = args + tuple(kwargs.items())
		if cache_key not in wrapper_cache.cache:
			wrapper_cache.cache[cache_key] = func(*args, **kwargs)  # 4.CountCalls(fibonacci)(*args, **kwargs)
		return wrapper_cache.cache[cache_key]

	wrapper_cache.cache = dict()
	return wrapper_cache


@cache  # 2.1 fibonacci = cache(CountCalls(fibonacci)) --> fibonacci = wrapper_cache
@CountCalls  # 1.1 fibonacci = CountCalls(fibonacci)
def fibonacci(num):
	if num < 2:
		return num  # 当num = 0时,fibonacci(0) = 0;当num = 1时,fibonacci(1) = 1;当num = 2时,fibonacci(2) = 1
	return fibonacci(num - 1) + fibonacci(num - 2)


def main():
	print(fibonacci(5))  # 3. wrapper_cache(5)


if __name__ == '__main__':
	main()

4. 添加单位信息

5. 验证JSON数据

二、参考资料

猜你喜欢

转载自blog.csdn.net/weixin_37780776/article/details/106535717
今日推荐