Python小技巧:​args 和 kwargs 的乐趣

Python与算法社区

共 2900字,需浏览 6分钟

 ·

2022-05-20 21:57


这篇文章是伟兄给我的稿子,总结实用、到位。另外,欢迎访问并关注他的博客:
https://jl-zhenlaixiaowei.blog.csdn.net/
我曾经和一个聪明的 Pythonista 结对编程,每次他输入带有可选或关键字参数的函数定义时,他都会惊呼“argh!”和“kwargh!”。
要不然我们相处的很好,我猜想这就是学术界编程最终对人所带来的影响吧。
现在argskwargs参数仍然是 Python 中非常有用的特性,而且理解它们的威力将使您成为更有效的开发人员。

那么“args”和“kwargs”参数用来做什么呢?

它们允许一个函数接受可选参数,因此你能够在你的模块和类里创建弹性APIs。
示例代码如下:
In [2]: def foo(required, *args, **kwargs):
   ...:     print(required)
   ...:     if args:
   ...:         print(args)
   ...:     if kwargs:
   ...:         print(kwargs)
上面的函数需要至少一个叫做“必须的”参数,但是它也能接受额外的位置参数和关键字参数。
如果我们调用带有附加参数的函数,参数将会收集额外的位置参数作为一个元组,因为这个参数的名字有一个*(单星号)前缀。
同样地,kwargs将收集额外的关键字参数作为一个字典,因为这个参数名字有**(双星号)前缀。
如果没有附加参数被传递给函数。args 和 kwargs 可以为空。
当我们调用带有参数的不同组合的函数时,你会看到在args和kwargs内部参数。
Python如何收集它们,根据它们是否为位置参数或者关键字参数。代码如下:
In [3]: foo()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
-3
-c19b6d9633cf> in 
----> 1 foo()

TypeError: foo() missing 1 required positional argument: 'required'
In [4]: foo('hello')
hello
In [5]: foo('hello'123)
hello
(123)

In [6]: foo('hello'123, key1='value', key2=999)
hello
(123)
{'key1''value''key2'999}
我要明确一下,调用args和kwargs参数是简单的命名惯例。
像之前的例子里,如果称他们*parms和**argv也可以。实际上语法分别是单星号(*)或者双星号(**)。
然而,我还是推荐你还是坚持可接受的命名惯例以避免混淆。(而且每隔一段时间还有机会喊“argh!”和“kwargh!”)。

## 转发可选或者关键字参数

有可能从一个函数到另一个函数传递可选或者关键字参数。
当你调用要转发参数的函数时,你可以通过使用解包参数操作符*和**。在你传递之前这也给你一个机会修改参数。
示例如下:
In [8]: def foo(x, *args, **kwargs):
   ...:     kwarg['name'] = 'Alice'
   ...:     new_args = args + ('extra', )
   ...:     bar(x, *new_args, **kwargs)
这种技术对于子类化和编写包装函数很有用。
例如,您可以使用它来扩展父类的行为,而不必在子类中复制其构造函数的完整签名。
如果您使用的 API 可能会在您的控制之外发生变化,这会非常方便,示例代码如下:
In [9]: class Car:
   ...:     def __init__(self, color, mileage):
   ...:         self.color = color
   ...:         self.mileage = mileage
   ...: 

In [10]: class AlwaysBlueCar(Car):
    ...:     def __init__(self, *args, **kwargs):
    ...:         super().__init__(*args, **kwargs)
    ...:         self.color = 'blue'

In [12]: AlwaysBlueCar('green'48392).color
Out[12]: 'blue'
AlwaysBlueCar 构造函数只是将所有参数传递给它的父类,然后覆盖一个内部属性。
这意味着如果父类构造函数发生更改,AlwaysBlueCar 很有可能仍会按预期运行。
这里的缺点是 AlwaysBlueCar 构造函数现在有一个相当无用的签名——如果不查找父类,我们不知道它需要什么参数。
通常,您不会将这种技术用于您自己的类层次结构。更有可能的情况是您想要修改或覆盖某些您无法控制的外部类中的行为。
但这总是危险的领域,所以最好小心(否则你可能很快就会有另一个理由尖叫“argh!”)。
这种技术可能有用的另一种情况是编写包装函数,例如装饰器。在那里,您通常还希望接受要传递给包装函数的任意参数。
而且,如果我们可以在不必复制和粘贴原始函数签名的情况下做到这一点,那可能更易于维护:
import functools
def trace(f):
    ...:     @functools.wraps(f)
    ...:     def decorated_function(*args, **kwargs):
    ...:         print(f, args, kwargs)
    ...:         result = f(*args, **kwargs)
    ...:         print(result)
    ...:     return decorated_function
    
In [11]: @trace
    ...: def greet(greeting, name):
    ...:     return '{}, {}!'.format(greeting, name)
In [14]: greet('Hello''Bob')
0x7fefa69db700
> ('Hello''Bob') {}
Hello, Bob!
使用像这样的技术,有时很难在使代码足够明确的想法和遵守不要重复自己(DRY)原则的想法之间取得平衡。
这可能永远是一个艰难的选择。如果你能从同事那里得到第二个意见,我鼓励你尝试一下。

浏览 43
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报