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

文章目录

  • 装饰器直观解释与理解
  • 装饰的提出背景,以及同继承的关系
  • 从装饰中函数名变化看装饰过程
      • case 1: 同模式多次装饰
      • case 2: 同一个函数不同模式装饰
      • case 3: 尝试观察调用wrapper带来的多义性问题
  • 装饰的本质:高阶函数和NoneType问题
    • 装饰的高阶函数实质
    • NoneType装饰器
    • 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
装饰器可以在原有函数的基础上,保证其原功能不变的同时,将另一些新功能加入函数,批量定义有相同结构或特性的函数。
我们可以将这个过程理解成为:用wrapper()替换了被@log装饰的新定义函数now()

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

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

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

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

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

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

两类的解决办法如图:

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

这种方法解决了继承体系的臃肿(如上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()

Python 装饰器的若干补充:装饰模式,functools.wraps,以及‘NoneType‘ object is not callable问题_第2张图片
这也就是说,不必担心……因为这是装饰器内部的局部函数。

装饰的本质:高阶函数和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

如果非要这么用,在调用的时候,不能以函数的规则使用它。
但这种功能缺失并不是说百害无一利。如果需要装饰的函数是无参的,完全可以通过这种紧缩来使得代码更加简洁~ 也可见于文末的示例。我们把这戏称为使能换耗能

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

这里尝试将中间层消灭掉,把具有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),相当于没有对应参数,因而最终得到了一个空值。尽管这个空值可以作为调用这个装饰器内部功能的句柄。

NoneType装饰器的调用

调用这个被修饰的“函数”的时候,就不能以函数的规则来使用了,否则会报错
Python 装饰器的若干补充:装饰模式,functools.wraps,以及‘NoneType‘ object is not callable问题_第3张图片
使用过程,相当于完成一个变量的自身运算。

另外:

  • 如果将其中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

你可能感兴趣的:(Python,python,设计模式)