装饰接受参数的函数
如何装饰带有参数的函数呢?
Python中用于变长参数的*args和**kwargs特性就能派上用场了:
这个装饰器有两个值得注意的地方:
它在wrapper闭包定义中使用*和**操作符收集所有位置参数和关键字参数,并将其存储在变量args和kwargs中;
接着,wrapper闭包使用*和** “参数解包”操作符将收集的参数转发到原输入函数。
现在将proxy装饰器中介绍的技术扩展成更有用的示例。下面的trace装饰器在执行时会记录函数参数和结果:
使用trace对函数进行装饰后,调用该函数会打印传递给装饰函数的参数及其返回值:
这仍然是一个简单的演示示例,不过有助于调试程序。
如何编写“可调式”的装饰器
在使用装饰器时,实际上是使用一个函数替换另一个函数。这个过程的一个缺点就是“隐藏”了(未装饰)原函数所附带的一些元数据。例如,包装闭包隐藏了原函数的名称,文档字符串和参数列表:
如果试图访问这个函数的任何元数据,看到的都是包装闭包的元数据:
这增加了调试程序和使用Python解释器的难度。幸运的是,有一个方法能避免这个问题:使用Python标准库中functools.wraps装饰器。
在自己的装饰器中使用functools.wraps能够将丢失的元数据从被装饰的函数复制到装饰器闭包中:
将functools.wraps应用到由装饰器返回的封装闭包中,会获得原函数的文档字符串和其他元数据:
建议最好在自己编写的所有装饰器中都使用functools.wraps。可以减少自己和其他人的调试时间。
有关装饰器的内容就此完结!
有趣的*args和**kwargs
*args和**kwargs参数到底有什么用呢?它们能让函数接受可选参数,因此能在模块和类中创建灵活的API:
上述函数至少需要一个名为required的参数,但也可以接受额外的位置参数和关键字参数。如果用额外的参数调用该函数,args将收集额外的位置参数组成元组,因为这个参数名称带有*前缀。kwargs会收集额外的关键字参数组成字典,因为参数名称带有**前缀。如果不传递额外的参数,那么args和kwargs都为空。
参数args和kwargs只是一个命名约定。哪怕将其命名为*parms和**argv,前面的例子也能正常工作。实际起作用的是*和**操作符
传递可选参数或关键字参数
可选参数或关键字参数还可以从一个函数传递到另一个函数。这需要用到解包操作符*和**将参数传递给被调用的函数。
参数在传递之前还可以修改:
这种技术适用于创建子类和编写包装函数。例如在扩展父类的行为时,子类中的构造函数不用再带有完整的参数列表,因而适用于处理那些不受我们控制的API:
AlwaysBlueCar构造函数只是将所有参数传递给它的父类,然后重写一个内部属性。这意味着如果父类的构造函数发生变化,AlwaysBlueCar仍然可以按预期运行。
不过缺点是对于AlwaysBlueCar构造函数如果不查看父类,就不知道函数接收哪些参数。一般情况下,自己定义的类层次中并不会用到这种技术。这通常用于修改或覆盖某些外部类中的行为,而自己又无法控制这些外部类。
该技术可能有用的另一个场景是编写包装函数,如装饰器。这种情况下,我们通常也想接受所有传递给包装函数的参数:
*和**操作符有一个非常棒但有点神秘的功能,那就是用来从序列和字典中“解包”函数参数。
而使用*操作符进行函数参数解包能更好的地处理这种情况:
在函数调用时,在可迭代对象前面放一个*能解包这个参数,将其中的元素作为单独的位置参数传递给被调用的函数。
这种技术适用于任何可迭代对象,包括生成器表达式。在生成器上使用*操作符会消耗生成器中的所有元素,并将它们传递给函数:
类似的还有**操作符,用于从字典中解包关键字参数:
由于字典时无序的,因此解包时会匹配字典键和函数参数:x参数接受字典中与“x”键相关联的值。如果使用*操作符解包字典,则所有的键将以随机顺序传递给函数:
Python的函数参数解包功能带来了很多灵活性。不一定要为程序所需的数据类型实现一个类,使用简单的内置数据结构(如元组或列表)就足够了,这样有助于降低代码的复杂度。
今天就到这了,感谢阅读,下节见!