Python 装饰器的若干补充:装饰模式,functools.wraps,以及‘NoneType‘ object is not callable问题

装饰器直观解释与理解

廖大的代码如下:

def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

@log
def now():
    print('2015-3-25')

装饰器就是一种以函数为参数的函数,定义一定的变换规则,可以将函数进行替换,实现功能扩充,如本段代码中的log
装饰器可以在原有函数的基础上,保证其原功能不变的同时,将另一些新功能加入函数,批量定义有相同结构或特性的函数。
我们可以将这个过程理解成为:用wrapper()替换了被@log装饰的新定义函数now()

*args相当于C语言中的一维数组,在python中可以表示任意元组,包括单个数字或字串,**kwargs(key word argument)相当于二维数组,可以表示字典。因而包含了python中所有的数据类型。所以装饰器具有普适性

装饰的提出背景,以及同继承的关系

这个过程是用用一个公用的规则来创建一批具有部分类似结构的对象,很容易使人联想到继承。继承语法简单,不涉及高阶函数,可以说相对容易理解,但人们仍然在创立并保留了装饰,背后有其内在逻辑。

面向对象语言发展过程当中,继承先于装饰,是为了克服继承的复用性问题而作的。
这个问题主要集中在两个方面:

  1. 新的父类出现时,要进行同样规则的继承仍然要为其重新定制。
  2. 如果父类发生变更,那么其相关联的子类都会受到牵连。若子类并不需要作出相应的改变,这个问题将可能导致子类的重写。

在这里插入图片描述

两类的解决办法如图:

  1. 将“继承”的规则作为一种变换法则,称为装饰。
  2. 只装饰父类,使得其成为欲改造的新父类。避免了子类的牵连

这种方法解决了继承体系的臃肿(如上1),也避免了继承带来的连带关系,降低了代码依赖性(如上2),最终发展成为装饰模式

从装饰中函数名变化看装饰过程

通过廖大的这个例子,我们可以更接近装饰器的操作实质。

import functools


def log(text):
    def decorator(func):
        # @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator


@log('excute')
def now():
    print('2020-8-5')


now()
print(f"decorated name: {now.__name__}")

输出结果

excute now():
2020-8-5
decorated name: wrapper

操作过程是,被装饰的函数,按照装饰器的规则进行运算。装饰过程中,作为参数的函数名不变,但返回值为wrapper
所以如果对一些特定的情形,应该将变量名换回来,使用语句

@functools.wraps(func)

变量名的这个操作激发了我们对函数本体探索的欲望给了我们进一步研究装饰过程实质的新角度。

case 1: 同模式多次装饰

同一个装饰器可以作用于不同函数的定义。也就实现了复用

@log('re-excute')
def again():
    print('2020-8-6')

again()
print(f"decorated name: {again.__name__}")

输出

re-excute again():
2020-8-6
decorated name: wrapper

case 2: 同一个函数不同模式装饰

经过装饰之后,函数名都发生了改变,那么直接调用是否会产生多态?

经过下面这个尝试,然后猛然意识到只有定义时才可以装饰,也就是说装饰器先于装饰过程。

# def dec(func):
#     @functools.wraps(func)
#     def wrapper(*args, **kw):
#         print('rebuild %s():' % (func.__name__))
#         return func(*args, **kw)
#     return wrapper
# @dec
# func

case 3: 尝试观察调用wrapper带来的多义性问题

考虑到case1 中多函数公用装饰器的问题,可能会出现多义性,导致问题复杂化。我们简单地加以测试,在函数外部直接调用wrapper()

wrapper()

wrapper报错
这也就是说,不必担心……因为这是装饰器内部的局部函数。

装饰的本质:高阶函数和NoneType问题

装饰的高阶函数实质

装饰过程是一个高阶函数分层解析的过程。
比如:

def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

@log
def now():
    print('2015-3-25')

这个过程中,@log标识符就使得now作为二阶函数log()的参数,被里层的wrapper()调用并包装(调包)并且对外表现为wrapper(now)(*args, **kw)

即把log(now)转化为wrapper(now)

这个“调包”真的可以很巧妙地描述这个装饰过程。

另外,我们还可以使用带参的装饰器举例:

def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

这个装饰,将log(text)替代decorator,再用log(text)(func)替代wrapper,也就是调包两次,最终将func替换成一个以func为参数的函数

NoneType装饰器

这两个例子都指向一个要点,最内层的wrapper函数名,必须是一个以func为变量的函数。如果没有这个承接的wrapper或者实质上的wrapper,那么这个函数被装饰后成为一个空函数,类型为NoneType。NoneType函数将不能按函数的方式进行调用。

假如我突发奇想把高阶函数降阶,比如将带参的函数阶数减为二阶:

这里尝试将中间层消灭掉,把具有func为参数的函数名的decorator层剪掉

def log(level):
    def wrapper(func, *args, **kwargs):
        if level == 'info':
            print('info log')
        elif level == 'error':
            print("error log")
        # return func(*args, **kwargs)
    return wrapper


@log(level='error')
def test():
    print('func test')


if __name__ == '__main__':
    test
    # test() 这个会报错

这个例子中,最终func会被空值替代。
我的猜想是,返回值wrapper被调包后,函数实质为log(level)(func, *args, **kwargs),然而希望得到的返回值是log(level)(func)(*args, **kwargs),相当于没有对应参数,因而最终得到了一个空值。严格来说,也可以作为一个不合格Python程序员的罪证。但我们接下来仍然可以简单说说它的应用。顺便可以辩护他并不是忘了语法而是别具匠心的呐

这个句柄的功能,取决于wrapper函数本来的结构。比如代码中如果进行如下的操作:

return func(*args, **kwargs)

那么最终就会进行func本身的操作,在这个例子当中就是test的函数内部的输出。

NoneType装饰器的调用

慎用

看似是一个使用者的Bug,把一个函数都变成不是函数的东西了(?)
但为了面子这锅我可不能背
这种功能缺失并不是说百害无一利。如果需要装饰的函数无参(这在第二点中将说到),完全可以通过紧缩一层操作来使得代码更加简洁~ 这一点也可见于文末的示例。我们把这戏称为使能换耗能

具体使用过程,再简单地重新整理一下。

  1. 这个被修饰的函数退化成一个“抓手”,调用这个“函数”的时候,就不能以函数的规则来使用了,否则会报错:
    在这里插入图片描述
    应该去掉括号,像写一个变量一样,只不过它的内涵和行为都更加复杂。调用过程相当于一个变量完成自身运算。有点海象运算符内味儿

  2. 装饰的功能由另外关于被装饰函数及其参数:

    • 如果将其中return func(*args, **kwargs)一句删掉,那么最终结果将现实test函数并没有被运行。test函数的内部功能是通过func实现的。
    • 由于是空值,不能传入参数。如果都不用带参数,完全可以去掉其中的argskw,仍能正常运行。毕竟无参也包括在任意参数里。

      *args相当于C语言中的一维数组,在python中可以表示任意元组,包括单个数字或字串,**kwargs(key word argument)相当于二维数组,可以表示字典。

    • 如果被装饰函数需要参数,就没办法了。

当然,总归不推荐这种使用方法(指NoneType装饰器)。尽管其装饰无参函数的时候,具有较好的简洁性:
一个栗子

def dec(function):
    print("start...")
    function()
    print("end...")
#修饰器
@dec
def say():
    print("say...")
#执行报错:TypeError: 'NoneType' object is not callable
say()

参考文档

https://www.jianshu.com/p/98f7e34845b5
https://blog.csdn.net/wth_97/article/details/82347803
https://www.jianshu.com/p/998ffc3f2423
https://blog.csdn.net/qq_27093465/article/details/53323187

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值