66、Python之函数高级:一个装饰器不够用,可以多装饰器buffer叠加

引言

最近有同学关心一个函数只能被一个装饰器装饰吗?能否同时使用多个装饰器进行装饰?又或者,在定义装饰器的时候,我们应该定义一个“瑞士军刀”式的超强装饰器,还是通过多个装饰器实现增强效果的“buffer”叠加。

今天这篇文章,我们就聊一下关于多装饰器的定义及使用。

本文的内容主要有:

1、单一职能原则

2、多装饰器的使用与注意事项

单一职能原则

在介绍多装饰器的使用之前,我想先简单谈一下面向对象设计原则中的“单一职能原则”。

前面我们曾经提及并简单介绍过面向对象设计的5个基本原则,也就是所谓的“SOLID原则”,这些原则的核心理念在于确保软件设计更加稳定及易于扩展和维护。这些原则分别是:

1、单一职能原则(Single Responsibility Principle, SRP)

2、开闭原则(Open / Closed Principle, OCP)

3、里氏替换原则(Liskov Substitution Principle, LSP)

4、接口隔离原则(Interface Segregation Principle, ISP)

5、依赖倒置原则(Dependency Inversion Principle, DIP)

其中,开闭原则我们已经反复提及到,也结合Python相关语法特性的使用,反复实践了开闭原则。

今天,我们来重点聊一下单一职能原则SRP。

SRP的核心思想是:一个类(模块)应该只有一个引起它变化的原因,也就是说一个类只负责一个职责或者功能。

换句话说,一个类应该只有一个变化的理由。将职责分离到不同的类中,可以使得每个类的目标更加明确、职责更加单一,从而提高代码的可维护性和可扩展性。

单一职责的出发点在于:

1、提高代码的可复用性:类的职责更加单一,本质上也就是更加高内聚、低耦合,使得每个单一职责的类可以像是乐高积木一样,在功能扩展时,可以进行快速的组装、复用。

2、提高代码的可维护性:可维护性其实已经蕴含了可读性,类的职责单一,降低了单个类设计、实现上的复杂度。

3、便于进行单元测试:每个类都天然是一个单一职责的单元,单元测试就变得更加简洁、自然。

为了实现单一职责的落地,可以从以下几个方面着手:

1、职责分离:这是该原则的基本,设计之初就将所有的职责做明确的切分。

2、模块化设计:单一职责的原则,必然要求了模块化的设计理念,单一职责所对应的功能封装到独立的模块或者类中,自然就进行了代码的模块化构建。

3、定期代码重构:很多时候,在设计开发过程中,为了快速上线,或者其他的原因,我们会尽量避免过早优化。所以,不可避免的,可能导致部分功能、模块的设计、开发背离了单一职能原则。为了后续的可维护性和可扩展性,应当定期进行多职责耦合的类的识别及代码重构。

虽然,SRP是面向对象的设计原则,但是在结构化设计或者面向过程的开发中,我们在进行函数的定义及开发中,也是应当遵的。

所以,回到开篇提到的装饰器定义的选择,我们是应该定义一个大而全的瑞士军刀式的装饰器,还是应该定义多个单一职责的装饰?从SRP的角度来看,显然应该进行职责的分离,定义多个小而美的装饰器。

多装饰器的使用与注意事项

基于SRP,我们更加倾向于定义多个小而美的装饰器。但是,一个函数可以使用多个装饰器来装饰、增强吗?

其实,只要回顾一下我们之前提到的关于高阶函数、闭包、装饰器的原理,就知道,显然是可以使用多个装饰器的。多个装饰器作用于同一个函数对象,其实就是对原始函数对象的多层套娃,最终还是函数对象。而且,任意一层套娃得到的函数对象,其函数签名与原始函数签名是一致的。

我们前面分别定义了使用缓存和记录日志的装饰器,我们可以看一下两个装饰器放在一起使用的效果。

为了便于查看,我们对日志装饰器进行简化,直接看代码:

1、日志装饰器函数定义 log.py:

# 简化一下日志记录的定义,便于查看
def log(func):
    def wrap(*args, **kwargs):
        print(f'log>> 函数{func.__name__}被调用 参数: {args},{kwargs}')
        try:
            res = func(*args, **kwargs)
            print(f'log>> 函数{func.__name__}被调用 返回值: {res}')
            return res
        except Exception as e:
            print(f'log>> 函数{func.__name__}被调用 发生异常: {e}')

    return wrap

2、缓存装饰器 cache.py:

def cache(func):
    mem = {}

    def wrap(*args):
        res = mem.get(args)
        if not res:
            res = mem[args] = func(*args)
        else:
            print(f"cache>> 参数[{args}]命中缓存")
        return res

    return wrap

3、入口文件进行斐波那契函数的计算:

from log import log
from cache import cache


@log
@cache
def fibonacci(n):
    if n == 1:
        return 0
    if n == 2:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)


if __name__ == '__main__':
    # 查看函数对象信息
    print(fibonacci)
    fibonacci(5)

执行结果:

146cdcaaac0ac55ad03b297c400968ec.jpeg

如果我们交换一下装饰器的顺序,则执行结果会发生变化:

7b37675bcf2b9144b82a20fc80800f57.jpeg

基于上面的执行过程,我们可以得到以下认知:

多个装饰器同时作用于一个函数时,越靠近函数,越先对函数进行封装

@cache
@log
def fibonacci(n):
    pass

等价于:fibonacci = cache(log(fibonacci))

最终得到的函数对象,是最外层装饰器封装的结果。

所以,只要理解了@装饰器名的语法糖背后的实现原理,无论装饰器的使用怎么组合、变换,我们都能轻易理解并掌握。

总结

本文以对“大而全”和“小而美”的设计的疑问为出发点,引出了对单一职责原则的理解与实践,并通过遵循SRP的装饰器设计,实现了多个装饰器对原始函数的buffer效果的叠加,看到更加灵活的组合使用。

感谢您的拨冗阅读,希望对您有所帮助。

2a498f6d72ab54da1dfd79e1ecc1712e.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南宫理的日知录

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值