为什么在Python中要使用**kwargs?与使用命名参数相比,有哪些真实世界的优势?

84

我来自静态语言的背景。有人可以解释一下(最好通过例子)使用 **kwargs 而不是命名参数的真实世界优势吗?

在我看来,这只会使函数调用更加含糊不清。谢谢。


1
在C语言中考虑可变参数--有时你不知道你的参数会是什么。 - Charles Duffy
8个回答

73

你可能想为一系列原因接受几乎任意命名的参数 - 这就是**kw形式的作用。

最常见的原因是将参数直接传递给你要包装的其他函数(装饰器是其中之一,但远不是唯一的!) - 在这种情况下,**kw减少了包装器和被包装函数之间的耦合,因为包装器不需要知道或关心被包装函数的所有参数。这里是另一个完全不同的原因:

d = dict(a=1, b=2, c=3, d=4)

如果所有的名称都必须事先知道,那么显然这种方法就不能存在了,对吧?顺便说一下,当适用时,我更喜欢用这种方式创建一个字典,其键是文字字符串,而不是:

d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}

原因很简单,因为后者包含了大量的标点符号,所以可读性较差。

如果没有任何接受**kwargs的充分理由,就不要接受它:就这么简单。也就是说,如果没有好的理由允许调用者传递具有任意名称的额外命名参数,请不要允许发生这种情况——只需避免在函数签名的结尾处使用**kw形式即可。

至于在调用中使用**kw,这可以让您独立于单个调用点,将必须传递的每个命名参数及其对应的值组合成一个字典,然后在单个调用点使用该字典。比较如下:

if x: kw['x'] = x
if y: kw['y'] = y
f(**kw)

发送至:

if x:
  if y:
    f(x=x, y=y)
  else:
    f(x=x)
else:
  if y:
    f(y=y)
  else:
    f()

即使只有两种可能性(而且非常简单!),缺少 **kw 已经使第二个选项变得绝对不可行和无法忍受 - 想象一下当可能有半打稍微更丰富的交互时情况会如何……在这种情况下没有 **kw,生活将是绝对的地狱!


1
+1 - 能够做到这一点非常有用,但如果你习惯于(比如)Java,那么这将是一种范式转变。 - ConcernedOfTunbridgeWells
一个字典可以作为参数使用,即使没有kwargs,下面的代码仍然可以工作。if x: kw['x'] = x; if y: kw['y'] = y; f(kw) - Vhon Newmahn

46

如果您正在子类中扩展现有方法,则可能希望使用**kwargs(和*args)。您想将所有现有参数传递给超类的方法,但希望确保即使在未来版本中更改签名,您的类仍然可以正常工作:

class MySubclass(Superclass):
    def __init__(self, *args, **kwargs):
        self.myvalue = kwargs.pop('myvalue', None)
        super(MySubclass, self).__init__(*args, **kwargs)

40

真实世界的例子:

装饰器 - 它们通常是通用的,因此您不能预先指定参数:

def decorator(old):
    def new(*args, **kwargs):
        # ...
        return old(*args, **kwargs)
    return new

在需要使用未知数量关键字参数进行一些魔法操作的场合。例如,Django的ORM就是这样做的:

Model.objects.filter(foo__lt = 4, bar__iexact = 'bar')

2
你说的“想要施展魔法”是什么意思?这里的“魔法”指什么?另外,我会将“魔法”与不良设计联系起来。Django的情况算是合理的设计吗? - joel

16

有两种常见情况:

第一种:您正在包装另一个接受多个关键字参数的函数,但是您只是要将它们传递下去:

def my_wrapper(a, b, **kwargs):
    do_something_first(a, b)
    the_real_function(**kwargs)

其次,您愿意接受任何关键字参数,例如在对象上设置属性:

class OpenEndedObject:
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

foo = OpenEndedObject(a=1, foo='bar')
assert foo.a == 1
assert foo.foo == 'bar'

5

**kwargs 在你不知道参数名称的情况下非常有用。例如,dict 构造函数使用它们来初始化新字典的键。

dict(**kwargs) -> new dictionary initialized with the name=value pairs
    in the keyword argument list.  For example:  dict(one=1, two=2)
In [3]: dict(one=1, two=2)
Out[3]: {'one': 1, 'two': 2}

2
如果你想要传递许多参数到另一个函数中,而你并不一定知道这些选项,那么它通常会被使用。例如,specialplot(a,**kwargs)可以将绘图选项在kwargs中传递给一个通用的绘图函数,该函数将这些选项作为命名参数接受。 - Tristan
2
虽然我认为这是不好的风格,因为它会阻碍内省。 - bayer
从基于静态IDE的开发角度来看,kwargs并不好。但是我可以想象到一个IDE可以静态地跟踪kwargs,并告诉你哪些关键字被哪个函数支持。 - aoeu256

3

这是我在使用CGI Python时的一个例子。我创建了一个类,将**kwargs传递给__init__函数。这使得我能够使用类在服务器端模拟DOM:

document = Document()
document.add_stylesheet('style.css')
document.append(Div(H1('Imagist\'s Page Title'), id = 'header'))
document.append(Div(id='body'))

唯一的问题是,由于“class”是Python关键字,因此您无法执行以下操作。
Div(class = 'foo')

解决方案是访问底层字典。
Div(**{'class':'foo'})

我并不是在说这是该功能的“正确”使用方式。我想说的是,有许多意想不到的方式可以利用这样的功能。


只是一点小提示:像Kotlin这样的现代语言允许你在定义或以命名方式传递时使用保留关键字作为字段或变量,只需使用反引号将它们括起来即可。 - undefined

2

以下是另一个典型的例子:

MESSAGE = "Lo and behold! A message {message!r} came from {object_} with data {data!r}."

def proclaim(object_, message, data):
    print(MESSAGE.format(**locals()))

0
一个例子是实现python-argument-binders,用法如下:
>>> from functools import partial
>>> def f(a, b):
...     return a+b
>>> p = partial(f, 1, 2)
>>> p()
3
>>> p2 = partial(f, 1)
>>> p2(7)
8
这是来自于 functools.partial Python 文档的内容:partial 与此实现“相对等价”:
def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = keywords.copy()
        newkeywords.update(fkeywords)
        return func(*(args + fargs), **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

这里,由于keywords是一个字典,.copy()只是创建了另一个指针,这意味着fkeywords被添加到了原始字典中。我认为你应该使用copy.deepcopy()来创建字典的实际副本,对吗? - aeroNotAuto
“柯里化”是“偏函数绑定”的俗称。 - smci

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接