Possible Duplicate:
“Least Astonishment” in Python: The Mutable Default Argument
今天下午我写了一些代码,在代码中偶然发现了一个错误。我注意到我新创建的一个对象的默认值是从另一个对象传递过来的!例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class One(object):
def __init__(self, my_list=[]):
self.my_list = my_list
one1 = One()
print(one1.my_list)
[] # empty list, what you'd expect.
one1.my_list.append('hi')
print(one1.my_list)
['hi'] # list with the new value in it, what you'd expect.
one2 = One()
print(one2.my_list)
['hi'] # Hey! It saved the variable from the other One! |
所以我知道这样做可以解决问题:
1 2 3
| class One(object):
def __init__(self, my_list=None):
self.my_list = my_list if my_list is not None else [] |
我想知道的是…为什么?为什么要对python类进行结构化,以便在类的实例之间保存默认值?
事先谢谢!
- 魏德,提醒我一个原型链在贾瓦斯克里普
- 这是Python功能的一个方面,而不是种类。无论如何,这篇报纸可以帮助澄清为什么Python设计这条路。
- 看起来像是我最后一次看这个问题的新版本
- 蟒函数(当方法或平面函数)是它们的对象。缺陷的论据与参数名称(如果呼叫提供了一个解释值的话,则阴影);其可见性是功能体。除了一个方法是定义类的一个成员的事实之外,在等级上没有发生任何事情。
这是Python默认值工作方式的一种已知行为,对于不谨慎的人来说,这通常是令人惊讶的。空数组对象[]是在定义函数时创建的,而不是在调用函数时创建的。
要修复它,请尝试:
1 2 3 4
| def __init__(self, my_list=None):
if my_list is None:
my_list = []
self.my_list = my_list |
- 注意,您的解决方案有一个潜在的bug:如果您将一个空列表传递给您的函数,目的是对象复制对该列表的引用,那么您的my_list or []表达式将选择新的空列表[]而不是my_list(因为空列表是错误的)。
- -1:这不是"问题"。这是一个定义问题。
- @洛特:谢谢,修正了术语。
- 我个人认为,在大多数情况下,if foo is None: foo = mutable_default是一种反模式。现在,函数只是意外地改变从外部显式传入的值。另外,你失去了实际通过None的能力,这可能有意义,也可能没有意义。
- @本+1,如果我可以的话再加一个。我更喜欢def func(arg=()): arg = list(arg); proceed()。假设首先需要一个可变的值。考虑到我们也应该让用户通过一个生成器,而没有一个令人信服的理由来禁止它……在这种情况下,我们通常需要复制数据,如果我们做的不是为了非变化的目的而在数据上迭代的话。
- 本:1。可变的默认值可以有记忆化的目的。我没有看到任何基于if foo is none: foo = []类参数处理的"从外部显式地改变值"的例子。我看到参数值的"可预测"变化。从不"意外"。
- @洛特:我不是那个说"永远不要使用可变的默认参数"的人。就我个人而言,我只是使用空列表作为默认参数,并且根本不改变函数的参数,除非这是可调用的文档化功能的一部分。我的观点是,如果您遇到一个由于可变的默认参数而导致的错误,那么在接收到非默认值的情况下,它很可能是一个错误,并且if foo is none: foo = []没有采取任何措施来解决这个问题,只会使错误变得更微妙。
- @本:"在接收到非默认值的情况下,这可能是一个错误。"我很难想象这一切。我真的不同意这种可能性。它必须是一个语义非常混乱的函数。这种想法甚至还有一些微妙之处,这就是真正的代码味道。与易变性无关。听起来它和模糊的函数语义有着千丝万缕的联系。
- @Lott:假设我有一个函数,它获取一个字符串列表,并将它们与其他格式一起写入一个文件。我允许列表默认为空。我发现多次调用它会改变默认值,所以我应用了None默认技巧。但只有当函数改变列表时,这才是必需的。如果是这样的话,如果我给它传递一个字符串列表,我想再次将它用于其他用途,会发生什么?它被击倒了。真正的问题不是默认值,而是函数不应该将其参数修改为其实际用途的副作用。
- @S.lott:在许多实际的项目中,函数都在模块A中,由编码员A编写,调用方在编码员B编写的模块B中,值源自编码员C编写的模块C。
- @本:"真正的问题不是默认值,而是函数不应该将其参数修改为其实际用途的副作用。"对的。这就是真正的代码味道。不做什么的好例子。与if foo is none: foo = []无关。一切都与糟糕的设计有关。坏设计的好例子。
- @格雷格·休吉尔:谢谢你对埃多克斯的建议。我在这个问题上改变了。对不起,我选择本的答案,因为它更好地解决了他和S.Lott在评论中所说的问题。
- @是的,没错。我只是选择站在我的肥皂盒上,因为我发现使用if foo is non: ...的需要通常是一个坏设计的迹象。每个人都喜欢帮助新的python程序员,解释默认值不是这样工作的,然后他们离开,把if foo is None: ...粘在任何地方,认为这意味着可以安全地改变他们的参数,在他们的代码中留下很少的时间炸弹(这通常永远不会消失,因为许多调用将使用默认值,并且许多ORE将使用调用后不重要的值,但这是原则)。
- @托雷特威德勒:嘿,谢谢。格雷格的答案可能是你问的问题的更好的答案(为什么会这样)。我的答案是在一个我认为通常应该和这个问题一起问的问题上提出的,但通常不是这样。:)
- @本:使用if foo is None:...的需要从来都不是坏设计的迹象。这总是正确的做法。你的设计糟糕的例子就是糟糕的设计。再也没有了。它们不是等待发生的事故。使默认参数发生"异常"变化的函数数量大约为零,因为这是一个糟糕的设计。请停止声称这是一个普遍的"问题"。他们"并没有随意、不加思考地将if foo is None:..."放入"他们的"代码中。请停止声称"他们"是。这个声明让所有的程序员听起来都很愚蠢。
- @那么为什么"可变默认参数陷阱"是学习Python的人常见的问题来源呢?如果人们没有改变他们的参数,他们永远不会发现默认参数并不是为每次调用都创建的。你说if foo is None: ...总是正确的。但是,只有当具有默认值的函数参数发生变化时,才需要使用它。我只是宣称,无论在这个特定的调用中是否接收到缺省值,改变这个参数都可能是不正确的。
- @本:人们不会随意、惊讶或意外地改变他们的论点。这是一个常见的问题,因为Python的可变性并不明显。可变参数是常见的。违约是有吸引力的。不是每个人都会犯错误。一旦他们看到它,他们就不会到处乱扔随机代码。大多数人似乎都明白了。有几个问这里。然后他们修好了。你的坏设计实例就是坏设计的好例子。你的进一步声明(江户十一〔六〕号普遍不好)是不合理的。许多人使用它并理解它。他们真的这么做了。
- @洛特:我没有说这是普遍的坏。我声称,对你的默认值发生变化感到惊讶通常意味着有一个更深层次的问题,if foo is None: ...没有解决这个问题。我还认为,如果论点有合理的默认值,那么就不太可能(并非不可能)改变该论点。所以,并不是因为None作为默认值,我认为代码是坏的。有人对一个突变的违约感到惊讶,并通过应用None违约来修复它,他很可能仍然有一个潜在的bug。
- @本:"对于一个突变的默认值感到惊讶并通过应用非默认值来修复它的人,可能仍然有一个潜在的bug。"对的。我声称这种情况非常罕见。我从没见过。你没有例子。但是,你重复了很多次,所以我放弃了。我从没见过。但是,你重复了很多次,我不得不同意。在某个地方,"他们"真的在向这个问题扔随机代码。这一定是因为你声称这是真的。我从来没见过,但我屈从于你令人筋疲力尽的重复。
- @ S.洛特。这是一个关于这个问题的问题。我们正在评论一个建议使用None默认技巧来解决问题的答案。这个问题的另一个答案是链接到一篇相对知名的文章,该文章说"有一个python gotcha,当他们学习python时,它会咬每个人。事实上,我认为正是蒂姆·彼得斯建议每个程序员都被它抓住两次。这就是所谓的可变默认陷阱。"卡尔·克奈赫特尔在对这个问题的评论中说,"最近几天我似乎一直在看到这个问题的新版本。"我不是在编这个。
- @本:我从没见过标准解决方案变成问题。但是,您已经发布并重新发布了标准解决方案会产生某种性质的问题。我没有证据。你没有证据。你所拥有的一切就是不断重复你的说法,"他们走了,把埃多克斯(EDOCX1)(1))粘在任何地方……在他们的代码中留下一些定时炸弹。你没有证据。我没有证据。你有无数的重复支持你,因此,你是对的。简单的说对了。不管这个解决方案有什么坏处,我都同意。
- @S.lott:根据定义,任何需要None默认值的代码都可以修改其参数,这是不好的,除非调用方知道这一事实。这就是我要说的。我非常清楚这一核心点,而你从未真正对此提出过异议。但你是对的,这是重复性的,我认为除了我们以外没有其他人读过。你是正确的,它在实践中不会经常引起问题。我强烈反对这意味着它应该被忽视。我想我们得把它留在那里。谢谢你的讨论。
其他一些人指出,这是Python中"可变默认参数"问题的一个实例。基本原因是默认参数必须存在于函数的"外部"才能传递给函数。
但这个问题的真正根源与默认参数无关。任何时候,如果修改可变的默认值都是不好的,您真的需要问问自己:如果修改了显式提供的值,这会是不好的吗?除非有人非常熟悉您的类的胆量,否则以下行为也会非常令人惊讶(因此导致错误):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| >>> class One(object):
... def __init__(self, my_list=[]):
... self.my_list = my_list
...
>>> alist = ['hello']
>>> one1 = One(alist)
>>> alist.append('world')
>>> one2 = One(alist)
>>>
>>> print(one1.my_list) # Huh? This isn't what I initialised one1 with!
['hello', 'world']
>>> print(one2.my_list) # At least this one's okay...
['hello', 'world']
>>> del alist[0]
>>> print one2.my_list # What the hell? I just modified a local variable and a class instance somewhere else got changed?
['world'] |
10次中有9次,如果你发现自己在使用None作为默认值并使用if value is None: value = default的"模式",你不应该这样做。你不应该修改你的论点!参数不应该被视为被调用代码的所有者,除非它被明确地记录为拥有它们的所有权。
在这种情况下(尤其是因为您正在初始化一个类实例,所以可变变量将使用很长时间,并被其他方法和可能从实例中检索它的其他代码使用),我将执行以下操作:
1 2 3
| class One(object):
def __init__(self, my_list=[])
self.my_list = list(my_list) |
现在,您要从作为输入提供的列表中初始化类的数据,而不是取得一个预先存在的列表的所有权。两个独立的实例最终共享同一个列表,或者列表与调用者中的变量共享(调用者可能希望继续使用该变量),都没有危险。您的调用者还可以提供元组、生成器、字符串、集合、字典、自制的自定义可重写类等,这也有很好的效果,而且您知道您仍然可以依靠self.my-list有一个append方法,因为您是自己创建的。
这里仍然存在一个潜在的问题,如果列表中包含的元素本身是可变的,那么调用者和这个实例仍然会意外地相互干扰。我发现在我的代码实践中,这并不是一个经常出现的问题(所以我不会自动地对所有内容进行深入的复制),但是你必须意识到这一点。
另一个问题是,如果我的_列表非常大,那么拷贝可能很昂贵。在那里你必须权衡一下。在这种情况下,最好还是使用传入列表,并使用if my_list is None: my_list = []模式来防止所有默认实例共享一个列表。但是,如果您这样做了,您需要在文档或类的名称中明确表示,调用方正在放弃其用于初始化实例的列表的所有权。或者,如果您真的想构建一个列表只是为了封装在一个One的实例中,那么也许您应该考虑如何在One的初始化中封装列表的创建,而不是首先构造它;毕竟,它实际上是实例的一部分,而不是初始化值。但有时这还不够灵活。
有时,您真的希望进行别名操作,并且让代码通过改变它们都可以访问的值进行通信。然而,在我致力于这样一个设计之前,我想得很努力。它会让其他人吃惊(当你在x个月内回到代码中时),所以文档也是你的朋友!
在我看来,向新的Python程序员介绍"可变默认参数"gotcha实际上(稍微)有害。我们应该问他们"你为什么要修改你的论点?"(然后指出默认参数在Python中的工作方式)。一个函数有一个合理的默认参数,这通常是一个很好的指标,表明它并不是用来接收一个预先存在的值的所有权的,所以不管它是否得到默认值,它可能都不应该修改这个参数。
- 我同意你关于对象所有权的建议,但是你会得到你在任何时候传递对可变对象的引用时所描述的那种行为,而且这在任何语言中都是相当正常的——你已经习惯了。可变的默认陷阱是阴险的,因为它是非故意的,而其他语言不这样做。
- 但这只是我的观点。它咬你是因为你对默认参数不小心。但是,如果您要改变传入的值,那么函数的目的几乎总是改变传入的值。在这种情况下,使用默认值是不明智的。如果有一个bug,你意外地改变了一个默认值,那么可能有一个更微妙的bug,你意外地改变了一个别人关心的传入值。
- @本:我喜欢你的回答,但我有个问题。我的代码的目的实际上是成为一个工厂函数。有没有良好的做法,使工厂功能,我应该遵循?比如不使用__init__?
- @Toreltwiddler:我增加了一个部分,说明我将如何处理你的One类,以及其他需要考虑的事情(不幸的是,这是一种权衡)。希望有帮助!我还摆脱了关于工厂功能的评论,这可能有点令人困惑。我指的是,如果您希望每次参数都提供一个新值,那么参数本身可能是一个工厂函数(默认值为lambda: [])。但这很少是你真正想做的,所以从我的答案中编辑出来。
- @本:谢谢你详细阐述你的答案!在阅读了您最新的编辑之后,我确信在我的代码中没有重要的理由允许您将可变对象传递给它(谁的所有权将被接管)。初始化类后,我将填充列表和字典,以完全避免更改传递的对象时出现任何问题。再次感谢您的全面回答!
基本上,python函数对象存储了一个默认参数的元组,这对于整数等不可变的事物来说是很好的,但是列表和其他可变对象通常在适当的位置进行修改,从而导致您观察到的行为。
这是默认参数在Python中任何位置(而不仅仅是在类中)的标准行为。有关详细说明,请参阅函数/方法参数的可变默认值。
python函数是对象。函数的默认参数是该函数的属性。因此,如果参数的默认值是可变的,并且在函数内部进行了修改,那么这些更改将反映在对该函数的后续调用中。
这不是一个答案,但值得注意的是,对于在任何类函数之外定义的类变量也是如此。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13
| >>> class one:
... myList = []
...
>>>
>>> one1 = one()
>>> one1.myList
[]
>>> one2 = one()
>>> one2.myList.append("Hello Thar!")
>>>
>>> one1.myList
['Hello Thar!']
>>> |
注意,myList的值不仅持续存在,而且myList的每个实例都指向同一个列表。
我自己也遇到了这个bug/特性,花了大约3个小时的时间试图弄清楚到底发生了什么。在获取有效数据时进行调试是相当困难的,但这不是来自本地计算,而是以前的计算。
更糟的是,这不仅仅是一个默认参数。你不能只把myList放在类定义中,它必须被设置为等于某个值,尽管它被设置为等于的值只被计算一次。
至少对我来说,解决方案是简单地在__init__中创建所有类变量。
- 这就是类变量的定义。它在类中定义,并为类保留一个值。而实例变量是在实例中定义的,并为实例保留一个值。--至于"它必须设置为等于某个东西",每个python标签都必须设置为等于某个东西。如果此时不需要其他值,请将其设置为等于None。--"bug"是指您希望Python的行为与您使用过的其他语言类似。python不是另一种语言,它是python。