函数修饰器

参考链接:Python学习笔记11:函数修饰符 - 魔芋红茶 - 博客园 (cnblogs.com) 

目录

接收和返回函数

可变参数列表

内部函数

函数修饰符


Python有很多有趣的特性,其中函数修饰符就是一个。

我们在之前的那个web应用示例中用过如下写法:

@web.route('/log')

@符号后边的,就是一个函数修饰符,它可以在不改变原有函数的情况下改变函数的行为(通常来说是增强函数的行为)。

我们下面就来说说怎样实现一个函数修饰符。

在这之前,我们要先介绍几个必须要先掌握的内容。

接收和返回函数

如我们在Python学习笔记0:变量中讨论的,在Python中,所有的一切都是对象,而函数也不例外。那理所应当的,函数也可以作为对象被另一个函数所接收和返回。

我们看下边的例子:

def receiveFunc(func):
    print(type(func))
    return func

def hellow():
    print("hellow world")

returned=receiveFunc(hellow)
returned()

输出

<class 'function'>
hellow world

我们可以看到,receiveFunc接收了函数hellow并打印出其类型,然后返回,返回的函数依然可以正常执行并输出结果。

可变参数列表

我们都知道,在函数签名的定义中,参数数目可以是一个,也可以是多个,但都是定义的时候就指定的,但Python还有另一种定义方法,可以接收数目不定的参数。

def getMultipParam(*multipParams):
    for param in multipParams:
        print(param,' ',end='')
    print()
    
getMultipParam(1,2,3)
getMultipParam('a','b','c','d','e')

输出

1 2 3
a b c d e

通过类似*params这种方式,可以指定参数接收的是可变长度的参数列表,这种方式有点像C++中的指针,可以接收一个数组作为参数,而在Python这里,就是接收一个列表,本质上接收函数会把收到的参数列表转变为一个列表进行处理。

当然,你也可以直接传一个列表过去,但是需要用*来指定:

def getMultipParam(*multipParams):
    for param in multipParams:
        print(param,' ',end='')
    print()
    
getMultipParam(*[1,2,3])
getMultipParam(*['a','b','c','d','e'])

输出

1 2 3
a b c d e

可以看出两者是等价的。但是这么做是有意义的,其目的在于要用*来说明传过去的列表是作为可变参数列表来传递,而非是仅仅一个普通参数。当然,如果不这么做接收函数就会认为仅仅传入了一个参数,是个列表。

到这里,关于可变参数的内容说完了吗?并没有。

如果大家还记得,参数传递还有另一种方式,即指明参数名称,进行一对一的传递。如果是那种方式,可变参数要如何定义呢?

def getKVParams(**kvParams:dict):
    for paramKey,paramVal in kvParams.items():
        print(paramKey,':',paramVal,end=',')
    print()
    
getKVParams(name="jack",age=16)
getKVParams(career="enginner",age=18)

输出

name : jack,age : 16,
career : enginner,age : 18,

和之前的定义类似,不过是将传入参数作为字典来处理,相应的,也同样可以直接用**可变参数列表作为字典来传入:

def getKVParams(**kvParams:dict):
    for paramKey,paramVal in kvParams.items():
        print(paramKey,':',paramVal,end=',')
    print()
    
getKVParams(**{"name":"jack","age":16})
getKVParams(**{"career":"teacher","age":17})

输出

name : jack,age : 16,
career : teacher,age : 17,

你以为到这里就结束了?

太天真,我们还可以把这两者结合起来!

def getEveryParams(*multipParams,**kvParams:dict):
    for param in multipParams:
        print(param,end=',')
    print()
    for paramKey,paramVal in kvParams.items():
        print(paramKey,':',paramVal,end=',')
    print()
    
getEveryParams(1,2,3,age=16,name="jack")

输出:

1,2,3,
age : 16,name : jack,

可以看到,通过将两种方式结合,我们的函数就可以接收任何形式的传参了,至于这有什么用,不用急,看完这篇博客就能明白了。

关于前置知识,还有最后一个,内部函数。

内部函数

相信对Java不陌生的朋友应该会在此时说,这个我会,内部类!

不错,Python的内部函数在我看来和Java的内部类颇为类似,而且事实上两者也是一致的,因为我一直所强调的,在Python里,一切都是对象,所以Python会存在和Java内部类高度类似的东西也不足稀奇。

def getInnerFunc():
    def innerFunc():
        print("hellow world")
    return innerFunc

test=getInnerFunc()
test()

输出

hellow world

可以看到,getInnerFunc函数的内部定义了一个内部函数,然后将这个函数返回,而外部程序拿到返回的函数以后也可以正常执行。而这种使用方式也恰恰是内部函数的最常用方式。

函数修饰符

好了,终于要讲到我们的主题了,如何构造一个我们自己的函数修饰符。

其实其核心要点就是把我们之前讲的三种技巧结合起来。

我们始终要有这样的概念,函数修饰符的作用是将一个已有的函数增强其用途。

这种思想可以用现实的例子来类比,就拿我喜欢的军事来说吧。现在的主战坦克在真正作战中肯定不会裸奔,需要按作战行为增加诸多附加模块,比如悬挂附加装甲,增加自动反导弹装置,还有眼目发生器和红外干扰装置之类的更是稀松平常。

但是这些最重要的是什么,是我们在没有改变坦克本身的情况下增强了坦克的作战性能。

是不是很酷?

我们现在就在Python中这么做吧。

我们先定义一个坦克:

def tank(player1:'驾驶员',player2:'装填手',player3:'车长',player4:'炮长'):
    print("这是一个裸奔的坦克")
    print("该坦克有四名成员")

别问为什么这里没有用英语。。。

我们现在用坦克工厂给它加装反应装甲:

def tank(player1:'驾驶员',player2:'装填手',player3:'车长',player4:'炮长'):
    print("这是一个裸奔的坦克")
    print("该坦克有四名成员")

def tankFactory(tank:function)->function:
    def powerTank():
        tank()
        print("加装反应装甲")
        print("加装反导弹装置")
        print("加装红外干扰仪")
    return powerTank

可以看到,我们利用上边说的两个特性来构建了一个坦克工厂,来增强进入工厂的坦克。

等等,我们是不是忘掉了什么,对,我们这里忘记了使用可变参数,我们需要注意到,上边的示例是有问题的,比如从工厂返回的powerTank居然没有参数,这就相当于这个增强版坦克顶盖被焊死了,不能进驾驶员,这显然是不合理的,没有部队会接受这样的改装。

那我们像tank的定义一样指定四个成员作为参数行不行?当然是可以的,但是这同样有问题,比如现在一些先进坦克是自动装填,不需要炮手,只要三个成员,那你这工厂就不能接受这种坦克了,这相当有局限性。

所以,这里就是我们的可以接受任何类型参数的技巧的作用所在了,让我们改进一下我们的坦克工厂:

def tankFactory(tank:"function")->"function":
    def powerTank(*params:tuple,**kvParams:dict):
        tank(*params,**kvParams)
        print("已加装反应装甲")
        print("已加装反导弹装置")
        print("已加装红外干扰仪")
    return powerTank

@tankFactory
def tank(player1:'驾驶员',player2:'装填手',player3:'车长',player4:'炮长'):
    print("这是一个裸奔的坦克")
    print("该坦克有四名成员")

@tankFactory
def mordenTank(player1,player2,player3):
    print("这是一个3成员的现代坦克")

tank("tom","jerry","robot1","robot2")
mordenTank("tom","jerry","robot")

输出:

这是一个裸奔的坦克
该坦克有四名成员
已加装反应装甲
已加装反导弹装置
已加装红外干扰仪
这是一个3成员的现代坦克
已加装反应装甲
已加装反导弹装置
已加装红外干扰仪

可以看到,我们现在的坦克工厂非常棒,通过在要改装的坦克函数前简单的加上@tankFactory就可以直接改装。真是太方便了。

等等,在你准备撸袖子改装掉所有类型的坦克前,还需要注意一点,因为Python中所有函数都是对象,而作为函数修饰符的函数也不例外,而解释器在处理这种特殊的函数时,有时候会忘记这是一个函数修饰符。所以我们需要显示地告诉Python解释器,这是一个作为函数修饰符的特殊函数,而非普通货色。

当然,这有点奇怪,但我们要做的是接受它。

至于如何做,很简单:

functools模块引入一个函数wraps,并在函数修饰符中调用这个函数。

from functools import wraps
def tankFactory(tank:"function")->"function":
    @wraps(tank)
    def powerTank(*params:tuple,**kvParams:dict):
        tank(*params,**kvParams)
        print("已加装反应装甲")
        print("已加装反导弹装置")
        print("已加装红外干扰仪")
    return powerTank

@tankFactory
def tank(player1:'驾驶员',player2:'装填手',player3:'车长',player4:'炮长'):
    print("这是一个裸奔的坦克")
    print("该坦克有四名成员")

@tankFactory
def mordenTank(player1,player2,player3):
    print("这是一个3成员的现代坦克")

tank("tom","jerry","robot1","robot2")
mordenTank("tom","jerry","robot")

这就是最终的代码了。

关于函数修饰符的内容已经讲完了,但其实还有一些扩展性的问题可以讨论,比如函数修饰符这种叫法很怪异和别扭,其实叫做修饰器更妥当,因为其本质就是设计模式中的修饰器模式。还有函数修饰符和我们前边讲的上下文模式也颇为类似,是不是可以用函数修饰符实现一个上下文模式?