Python函数“记住”早期参数(**kwargs)

7

我有一些具有属性字典obj.attrs的对象。这些对象的构造函数接受一个字典和/或**kwargs以提高方便性。

它看起来像这样:

class Thing:
    def __init__(self, attrs={}, **kwargs):
        for arg in kwargs:
            attrs[arg] = kwargs[arg]
        self.attrs = attrs

这样 Thing({'color':'red'}) 就和 Thing(color='red') 一样。

我的问题在于构造函数会以某种方式“记住”上一次传递给它的 attrs 值。

例如:

>>> thing1 = Thing(color='red')
>>> thing2 = Thing()
>>> thing2.attrs
{'color': 'red'}

...但是thing2.attrs应该只是一个空字典!{}

这让我想知道,是否同时使用**kwargs和像attrs={}这样的参数会存在问题。

有什么想法吗?


2
与Python中的“最小惊奇”(可变默认参数)相同的原则。 - user395760
5个回答

14
使用默认参数的问题在于实际上只存在一个默认值实例。当您在init方法定义中使用attrs={}时,这个单个的默认{}实例是每次调用该方法的默认值(它不会每次都创建一个新的空字典作为默认值,而是使用同一个)。
问题是,如果只有一个attrs实例存在,并且对于Thing的每个实例,您都使用self.attrs = attrs,那么每个实例的self.attrs成员变量都指向共享的attrs默认实例。
另一个问题是,这完全是多余的吗?您可以使用**kwargs传递关键字/值参数或字典。如果您仅定义了这个:
class Thing:
    def __init__(self, **kwargs):
        for arg in kwargs:
            self.attrs[arg] = kwargs[arg]

这些策略仍然有效:

thing1 = Thing(color='red')

thing2 = Thing(**{'color':'red'})

my_dict = {'color' : 'red'}
thing3 = Thing(**my_dict)

所以,如果你简单地定义并使用Thing,你就可以完全避免你的问题。


那么我理解问题不在于 **kwargs,而是默认参数? - tjvr
1
正确。在使用默认参数时必须非常小心,因为默认参数的值在Python处理“def”语句时只创建一次,然后对同一默认参数的引用会反复使用。 - Brent Writes Code
1
我认为另一个重要的点是,你想做的一切都可以用 kwargs 来完成。你可能根本不需要 attrs - Brent Writes Code
被接受,因为它解释了问题并展示了解决方案。 - tjvr
太好了!真不敢相信我已经用Python编程三年了,竟然才遇到这个问题。 - MatthewKremer

2

改变签名,使字典每次创建。

class Thing:
    def __init__(self, attrs=None, **kwargs):
        self.attrs = attrs or {}
        self.attrs.update(kwargs)

你可以用 attrs.update(kwargs) 来代替 for 循环。 - martineau

2

attrs是一个字典的引用。当您创建一个新对象时,self.attrs指向该字典。当您从kwargs中分配一个值时,它会进入此字典。

现在,当您创建第二个实例时,它的self.attrs也指向同一个字典。因此,它获取该字典中的任何数据。

有关此的详细讨论,请参见stackoverflow上的“Python中最少惊奇”:可变默认参数。还可以在effbot上查看Python中的默认参数值


1

你想要修改你的代码为:

class Thing:
    def __init__(self, attrs=None, **kwargs):
        attrs = {} if attrs is None else attrs
        for arg in kwargs:
            attrs[arg] = kwargs[arg]
        self.attrs = attrs

正如其他人所指出的那样,缺省参数的值只在定义时被计算一次,而不是每次函数调用时都计算。通过使用可变容器,容器中的每个添加操作都会被所有后续调用看到,因为每个调用都使用相同的容器作为默认值。
也许你只是想使用 attrs 作为提供初始值的一种方式,而根本不打算共享字典。在这种情况下,请使用以下代码:
class Thing:
    def __init__(self, attrs=None, **kwargs):
        self.attrs = {}
        if attrs:
            self.attrs.update(attrs)
        for arg in kwargs:
            self.attrs[arg] = kwargs[arg]

真的很遗憾。现在我有一百个初始化例程要更改...不过还是谢谢! - tjvr
@blob8108: 我又想到了你真正想要做什么,已经在上面进行了编辑。 - Ned Batchelder

1

仅供参考 - 我们可以通过简单地不改变它来避免“attrs是一个共享可变对象”的问题。不要将kwargs倾倒到attrs中,而是将它们都倾倒到一个新字典中。然后默认参数对象将始终为{}

class Thing:
    def __init__(self, attrs = {}, **kwargs):
        self.attrs = {}
        # Don't write the loop yourself!
        self.attrs.update(attrs)
        self.attrs.update(kwargs)

我提到这个只是因为每个人都在赶着描述“使用None作为默认参数并检查它”的习惯用法,但我个人认为这种方法相当不专业。sgusc的想法是正确的:鉴于Python的**kwargs的普遍优越性,整个尝试是没有意义的。 :)

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