"最小的惊讶"和可变的默认论证

Ste*_*ini 2458 python language-design least-astonishment default-parameters

任何修补Python足够长的人都被以下问题咬伤(或撕成碎片):

def foo(a=[]):
    a.append(5)
    return a
Run Code Online (Sandbox Code Playgroud)

Python新手希望这个函数总能返回一个只包含一个元素的列表:[5].结果却非常不同,而且非常惊人(对于新手来说):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()
Run Code Online (Sandbox Code Playgroud)

我的一位经理曾经第一次遇到这个功能,并称其为该语言的"戏剧性设计缺陷".我回答说这个行为有一个潜在的解释,如果你不理解内部,那确实非常令人费解和意想不到.但是,我无法回答(对自己)以下问题:在函数定义中绑定默认参数的原因是什么,而不是在函数执行时?我怀疑经验丰富的行为有实际用途(谁真的在C中使用静态变量,没有繁殖错误?)

编辑:

巴泽克提出了一个有趣的例子.再加上你的大部分评论和特别是Utaal,我进一步阐述了:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]
Run Code Online (Sandbox Code Playgroud)

对我而言,似乎设计决策是相对于放置参数范围的位置:在函数内部还是"与它一起"?

在函数内部进行绑定意味着在调用函数时x有效地绑定到指定的默认值,而不是定义,这会产生一个深层次的缺陷:def在某种意义上,该行将是"混合"的(部分绑定)函数对象)将在定义时发生,并在函数调用时发生部分(默认参数的赋值).

实际行为更加一致:执行该行时,该行的所有内容都会得到评估,这意味着在函数定义中.

rob*_*rob 1554

实际上,这不是设计缺陷,并不是因为内部或性能.
它只是因为Python中的函数是第一类对象,而不仅仅是一段代码.

一旦你以这种方式思考,那么它就完全有意义了:一个函数是一个被定义的对象; 默认参数是一种"成员数据",因此它们的状态可能会从一个调用更改为另一个调用 - 与任何其他对象完全相同.

无论如何,Effbot 对Python中的默认参数值中出现这种行为的原因有一个非常好的解释.
我发现它非常清楚,我真的建议阅读它以更好地了解函数对象的工作原理.

  • 对不起,但任何被认为是"Python中最大的WTF"的**绝对是一个设计缺陷**.这是*每个人*在某些时候的错误来源,因为最初没有人期望这种行为 - 这意味着它不应该以这种方式设计开始.我不关心他们必须跳过什么箍,他们应该**设计Python,以便默认参数是非静态的. (266认同)
  • 无论它是否是一个设计缺陷,你的答案似乎暗示这种行为在某种程度上是必要的,自然而明显的,因为函数是一流的对象,而事实并非如此.Python有闭包.如果使用函数第一行的赋值替换默认参数,则会对每次调用的表达式求值(可能使用在封闭范围中声明的名称).完全没有理由认为每次以完全相同的方式调用函数时都会评估默认参数是不可能或合理的. (173认同)
  • 对于阅读上述答案的任何人,我强烈建议您花点时间阅读链接的Effbot文章.除了所有其他有用的信息之外,关于如何使用此语言功能进行结果缓存/记忆的部分非常方便! (75认同)
  • 即使它是一流的对象,人们仍然可以设想一种设计,其中每个默认值的*code*与对象一起存储,并在每次调用函数时重新评估.我不是说那会更好,只是作为一流对象的函数并不能完全排除它. (74认同)
  • 设计并不直接来自`函数是对象`.在您的范例中,提议将实现函数的默认值作为属性而不是属性. (20认同)
  • 这是一个设计缺陷。Effbot 的理由很糟糕。“如果您需要处理任意对象(包括 None),您可以使用哨兵对象” - 您不必编写代码来阻止语言首先执行一些不明显的操作。记忆化的例子很糟糕——记忆化是通过添加一个参数来实现的,这个参数实际上并不是作为参数,而只是为了利用所讨论的不明显行为的副作用。说“这个可怕的黑客不是设计缺陷,因为它启用了另一个可怕的黑客”是不令人信服的 (18认同)
  • 这是一种可怕的行为.如果我希望`foo(x = MyObject())`是`MyObject`的特定实例,我会声明`myobject = MyObject()`并定义`foo(x = myobject)`.唯一可能的用例是编写混淆代码. (12认同)
  • 我认为这是一个语言设计缺陷,原因很简单:函数定义应该在调用之间内部无状态.我说"内部无状态",因为如果通过引用传递数据或者作为调用其他状态更改方法的副作用,则可以在外部*更改状态.但是,在删除所有外部状态的情况下,函数的后续调用不应影响所述函数的未来行为.违反此规定将成为破坏代码模块化的责任(在关注点分离和函数式编程方面). (11认同)
  • @alexis,我还没有看到有人提出一个对象和参数声明的连贯模型来解决这个特殊情况而没有一堆其他明显不良的影响.这就是为什么我不同意这种行为可以被称为"糟糕的设计决定"; 它不仅仅是一个孤立的功能,而是Python的对象和功能模型的许多其他理想和有用的功能的结果. (6认同)
  • 我刚刚遇到了这个“设计选择”,这绝对是一个设计缺陷。 (6认同)
  • 不,这是一个设计选择,而不是一个明显的结果。例如,JavaScript 具有相同的功能,但提供了 OP 和大多数人期望的行为 - 默认值在每次调用时进行评估,就好像它们是在函数体的第一行执行的代码一样。JS 行为*内部*更加复杂(规范中有很多复杂的内容用于设置词法范围),但它为作者提供了更直观的功能。 (6认同)
  • 这不是设计缺陷.这是一个设计决定; 也许是一个糟糕的,但不是意外.状态就像任何其他闭包一样:闭包不是函数,而具有可变默认参数的函数不是函数.`x = [[1,2,3]]*5`也是bug的严重来源,但不是设计缺陷. (5认同)
  • 我否决了这个答案,因为这绝对是Python的一个设计缺陷。以至于 Python 的核心开发人员正在讨论[引入新语法来支持 Python 的后期绑定参数默认值](https://lwn.net/Articles/875441/)。 (5认同)
  • 我很惊讶没有人提到http://www.jeffknupp.com/blog/2013/02/14/drastically-improve-your-python-understanding-pythons-execution-model/让你更清楚地了解执行模型以及为什么`a`会"重新浮出水面"而不是每次调用都重新创建? (4认同)
  • @DanLenski这是一个可怕的想法,你应该使用闭包. (4认同)
  • @DanLenski,你可以做到这一点对你很好,但任何允许这对正常使用案件造成损害的东西都是一个设计缺陷.是的,这是故意的,但这是一个(罕见!)糟糕的设计决定. (4认同)
  • 当我发现这种行为时,我简直不敢相信。这是不直观的,并导致我(以及其他许多人,我)浪费了很多时间进行调试,只是发现我使用的构造函数保留了从一个调用到下一个调用的作为默认参数传递的列表的内容。问题是:_earth_上谁会想要这种行为?因为如果您找不到任何人,那么作为该语言的功能就没有意义 (4认同)
  • 可变数据结构很有趣。可变函数是令人难以置信的。 (4认同)
  • @MarkAmery的不同之处可能是,在Javascript中你也可以编写正常的函数.你不可能偶然写一台发电机.Python默认启用了更专业和更高级的行为,违反了最少惊喜的原则. (3认同)
  • 为什么这会与“第一类对象”相关?让默认参数成为 **定义** 阶段的一部分而不是 **执行** 阶段的一部分是一个**选择**(一个糟糕的选择),就像函数的主体,显然它不是即使函数是“第一类对象”,在“执行”阶段创建新列表也存在问题。这只是一个糟糕的选择,而且没有充分的理由。 (3认同)
  • 是的,我同意这是一个很重要的问题.我已经阅读了python列表中的讨论,很明显,由于复杂的原因,它们被推送到这个模型(并将其保存在python 3中).我只是没有发现在循环中生成函数的便利是一个决定性的论点. (2认同)
  • Thx,用于解释的链接.但是,我仍然被这种行为视为默认值而感到震惊.让我们希望这个(恕我直言)可怕的设计缺陷将在Python 4中修复.然后应该使用真实类重写这个模式的极少数有用的应用程序.我想,从一开始就完成它可以节省数百万行`如果x是None:x = foo()`,并且防止了很多错误 - 由那些没有RTFM的易犯错误的人制造 - 在在使用可变参数的模块中花费更多代码行. (2认同)
  • 许多语言具有一流的功能。这并不需要错误的设计决策,也没有一个可以做这样的事情。废话答案很多。 (2认同)
  • 所以Python函数有内部状态,很好,没有任何设计缺陷。我爱状态。一切都应该有状态。数学函数应该有状态。这会让世界变得更加容易。(最高等级的讽刺) (2认同)

Eli*_*ght 268

假设您有以下代码

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...
Run Code Online (Sandbox Code Playgroud)

当我看到吃的声明时,最令人惊讶的是认为如果没有给出第一个参数,它将等于元组 ("apples", "bananas", "loganberries")

但是,假设后面的代码,我会做类似的事情

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")
Run Code Online (Sandbox Code Playgroud)

然后,如果默认参数在函数执行而不是函数声明中被绑定,那么我会惊讶地发现水果已被改变(以非常糟糕的方式).这比发现foo上面的函数改变列表更令人惊讶的IMO .

真正的问题在于可变变量,并且所有语言都在某种程度上存在这个问题.这是一个问题:假设在Java中我有以下代码:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?
Run Code Online (Sandbox Code Playgroud)

现在,我的地图StringBuffer在放入地图时是否使用了键的值,还是通过引用存储了键?无论哪种方式,有人感到惊讶; 尝试将对象从Map使用中取出的值与他们放入的对象相同的人,或者即使他们使用的键实际上是同一个对象而无法检索其对象的人用于将其放入映射的(这实际上是Python不允许其可变内置数据类型用作字典键的原因).

你的例子是一个很好的例子,Python新人会感到惊讶和被咬.但我认为,如果我们"修复"这个,那么这只会产生一种不同的情况,即他们会被咬伤,而那种情况甚至会更不直观.而且,在处理可变变量时总是如此; 你总是遇到一些情况,根据他们正在编写的代码,某人可能直观地期望一种或相反的行为.

我个人喜欢Python当前的方法:默认函数参数在定义函数时进行评估,并且该对象始终是默认值.我想他们可以使用空列表进行特殊情况,但这种特殊的外壳会引起更多的惊讶,更不用说倒退不兼容了.

  • 实际上,我认为我不同意你的第一个例子.我不确定我是否喜欢首先修改这样的初始化器的想法,但是如果我这样做,我希望它的行为与你描述的一模一样 - 将默认值更改为`("blueberries","mangos" ")`. (40认同)
  • 我认为这是一个有争议的问题.您正在处理全局变量.在代码中涉及全局变量的任何地方执行的任何评估现在(正确地)引用("blueberries","mangos").默认参数可以像任何其他情况一样. (27认同)
  • 我发现这个例子具有误导性而非辉煌性.如果`some_random_function()`附加到`fruits`而不是赋值给它,那么`eat()`_will_的行为就会改变.对于当前的精彩设计来说太多了.如果您使用在其他地方引用的默认参数,然后从函数外部修改引用,那么您就会遇到麻烦.真正的WTF是人们定义一个新的默认参数(列表文字或对构造函数的调用)和_still_ get bit. (14认同)
  • 默认参数*与任何其他情况一样*.出乎意料的是,参数是全局变量,而不是本地变量.这又是因为代码是在函数定义时执行的,而不是调用.一旦你得到了,并且课程也是如此,那就非常清楚了. (11认同)
  • 你只是明确地声明了`global`并重新分配了元组 - 如果`eat`在那之后的工作方式不同,那绝对没什么好奇怪的. (11认同)
  • @EliCourtwright,这是我读过的这个问题的最佳答案.很好地解释.如你所示,这里不需要"修复".我关于如何不混淆新手的建议是发出关于可变内置类型作为默认参数的编译时警告,尽管这不是一个非常Pythonic的事情:-P (3认同)
  • @jpm,这就是为什么我把这个例子误称为误导.整个问题是关于*mutable*默认参数(正如这个答案甚至指出的那样). (3认同)
  • 尝试证明这个设计错误的另一个可怕的例子.我注意到这里的趋势!如果我看到'全球x; x = new_value`,我会变得非常谨慎,并且有点期待别的地方出现奇怪的行为.Python是动态的,如果你想要它很容易打破它.但它不应该自动射入脚中. (3认同)
  • 当我看到函数声明引用了 'fruits' 变量时,我想“好吧,程序员希望默认值是可变的。” 我很失望,事实并非如此。 (3认同)
  • @alexis`fruit`在这里故意是一个`tuple`来防止你描述的情况.`元组是不可改变的.它无法追加.这一选择也清楚地表明作者并不期望它会改变. (2认同)
  • @alexis 这里的例子的要点是展示为什么以其他方式工作在另一种情况下会同样令人惊讶或有问题。在这个特定示例中选择不可变类型可以更轻松地关注替代行为引起的范围问题,因为可变性不再是问题。故意回避并非旨在讨论的特定问题的示例并不具有误导性;他们是很好的例子。 (2认同)
  • @StefanoBorini 在某种程度上,这是一个见仁见智的问题,但语言设计者做出了非常好的选择。通过按照他们的方式做,他们*定位了您的错误可能造成的损害*。如此处所述,以另一种方式执行此操作会产生与*全局状态修改*相关的错误。全局状态修改问题可能很难追踪和修复,因此 Python 的决定迫使您非常明确地了解何时需要共享状态。按照他们的方式,您的错误存在于单个位置,很容易找到,并且很容易修复,所有这些都在单个函数的范围内。 (2认同)
  • 我不服气。我认为带有可变键的 Java 示例比可变函数参数更容易理解 (2认同)

glg*_*lgl 229

AFAICS尚未发布文档的相关部分:

执行函数定义时,将评估默认参数值.这意味着当定义函数时,表达式被计算一次,并且每次调用使用相同的"预先计算"值.这对于理解默认参数是可变对象(例如列表或字典)时尤其重要:如果函数修改对象(例如,通过将项附加到列表),则默认值实际上被修改.这通常不是预期的.解决这个问题的方法是使用None作为默认值,并在函数体中显式测试它[...]

  • 短语"这通常不是预期的"和"解决这个问题的方法"闻起来就像是在记录设计缺陷. (169认同)
  • 短语"这通常不是预期的"意味着"不是程序员实际想要发生的事情",而不是"不是Python应该做的事情". (30认同)
  • @bukzor:需要注意和记录陷阱,这就是为什么这个问题很好并且收到了很多赞成票的原因.与此同时,不一定需要删除陷阱.有多少Python初学者将列表传递给修改它的函数,并且看到变化显示在原始变量中感到震惊?然而,当你了解如何使用它们时,可变对象类型是很棒的.我想这只是归结为对这个特殊陷阱的看法. (6认同)
  • @Matthew:我很清楚,但这不值得陷阱.由于这个原因,您通常会看到样式指南和linters无条件地将可变默认值标记为错误.做同样事情的明确方法是将一个属性填充到函数(`function.data = []`)或更好,然后创建一个对象. (4认同)
  • @holdenweb哇,我晚会很晚。在上下文的情况下,bukzor是完全正确的:当他们决定语言应执行函数的定义时,他们在记录并非“预期”的行为/后果。由于这是他们设计选择的意外结果,所以这是设计缺陷。如果这不是设计缺陷,那么甚至不需要提供“解决方案”。 (3认同)
  • 我们可以用它来聊聊并讨论它可能是怎么回事,但是语义已经被彻底辩论,并且没有人能够为创建默认值调用提出合理的机制.一个严重的问题是,调用范围通常与定义完全不同,如果在调用时评估默认值,则名称解析不确定."绕行"意味着"你可以通过以下方式达到理想的目的",而不是"这是Python设计中的错误". (3认同)
  • @holdenweb 关于您 2014 年 12 月 19 日的评论:“不是 Python 应该做的”将描述一个实现缺陷,而不是一个设计缺陷。我们知道实现与设计相匹配——设计缺陷的问题首先关系到该设计是否是一个好主意。 (3认同)

Uta*_*aal 110

我对Python解释器内部工作一无所知(我也不是编译器和解释器方面的专家)所以如果我提出任何不可知或不可能的建议,请不要责怪我.

如果python对象是可变的,我认为在设计默认参数时应该考虑到这一点.实例化列表时:

a = []
Run Code Online (Sandbox Code Playgroud)

你希望得到一个由a引用的列表.

为什么a = [] in

def x(a=[]):
Run Code Online (Sandbox Code Playgroud)

在函数定义上实例化一个新列表而不是在调用上?就像你问"用户是否提供参数然后实例化一个新列表并使用它就好像它是由调用者生成"一样.我认为这是模棱两可的:

def x(a=datetime.datetime.now()):
Run Code Online (Sandbox Code Playgroud)

用户,你想一个默认为相应的,当你正在定义或执行到datetime X?在这种情况下,与前一个一样,我将保持相同的行为,就好像默认参数"assignment"是函数的第一条指令(在函数调用上调用datetime.now()).另一方面,如果用户想要定义时间映射,他可以写:

b = datetime.datetime.now()
def x(a=b):
Run Code Online (Sandbox Code Playgroud)

我知道,我知道:这是一个封闭.或者,Python可能会提供一个关键字来强制定义时绑定:

def x(static a=b):
Run Code Online (Sandbox Code Playgroud)

  • 这次真是万分感谢.我真的无法理解为什么这让我感到不安.你做得很漂亮,只需要少量的模糊和混乱.正如有人从C++中的系统编程中学习并且有时天真地"翻译"语言特征一样,这个虚假的朋友就像在课堂属性中一样,把我踢进了软件中.我理解为什么会这样,但我不禁厌恶它,无论它有什么积极的可能性.至少它与我的经历相反,我可能(希望)永远不会忘记它...... (17认同)
  • 你可以这样做:def x(a = None):然后,如果a是None,设置a = datetime.datetime.now() (11认同)
  • 在我的书中,规范结构不是怪癖或限制.我知道它可能是笨拙和丑陋的,但你可以称之为某种东西的"定义".动态语言对我来说似乎有点像无政府主义者:当然每个人都是自由的,但你需要结构才能让某人清空垃圾并铺平道路.猜猜我老了...... :) (6认同)
  • @Andreas一旦你使用Python足够长的时间,你就会开始看到Python将事物解释为类属性的方式是多么合乎逻辑 - 这只是因为C++(和Java等)语言的特殊怪癖和局限性. C#...)将`class {}`块的内容解释为属于*instances*:)是有意义的.但是当类是第一类对象时,显然自然是它们的内容(在内存中)以反映其内容(在代码中). (5认同)
  • 函数**定义**在模块加载时执行.函数**body**在函数调用时执行.默认参数是函数定义的一部分,而不是函数体的一部分.(嵌套函数会变得更复杂.) (4认同)
  • 在 python 函数签名中使用这样的“static”关键字是没有意义的。如果签名被评估*一次*(Python 是如何做到的),那么默认值也只评估一次,因此“静态”是多余的。如果签名*不*评估一次(Python 不这样做),那么每次调用函数时都会评估“static” - 这不会是静态的。 (2认同)
  • 作为一个学习Python的人,我理解了上面这个答案,但我无法理解这个;我发现它非常不清楚。这似乎是一个语言设计变更提案,混合了该语言当前如何工作的描述,并且不清楚您的哪些问题是反问句。(然后“这就像你在问一样”这句话后面是一个陈述而不是一个问题。) (2认同)

Len*_*bro 81

嗯,原因很简单,在执行代码时完成绑定,并且执行函数定义,以及......定义函数时.

比较一下:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)
Run Code Online (Sandbox Code Playgroud)

此代码遭受完全相同的意外事件.bananas是一个类属性,因此,当您向其添加内容时,它会添加到该类的所有实例中.原因完全一样.

这只是"如何工作",并且在功能案例中使其工作方式可能很复杂,并且在类的情况下可能不可能,或者至少减慢对象实例化的速度,因为你必须保持类代码并在创建对象时执行它.

是的,这是出乎意料的.但是一旦下降了,它就完全适合Python的工作方式.事实上,它是一个很好的教学辅助工具,一旦你理解为什么会发生这种情况,你就会更好地理解python.

这说它应该在任何优秀的Python教程中占据突出地位.因为正如你所提到的,每个人迟早都会遇到这个问题.

  • 如果每个实例的不同,则它不是类属性.类属性是CLASS上的属性.由此得名.因此,它们对于所有实例都是相同的. (18认同)
  • 如何在类中定义对于类的每个实例都不同的属性?(为那些无法确定不熟悉 Python 命名约定的人可能会询问类的正常成员变量的人而重新定义)。 (2认同)
  • @Kievieli:您在谈论类的普通成员变量。:-) 您可以通过在任何方法中说 self.attribute = value 来定义实例属性。例如 __init__()。 (2认同)

Bri*_*ian 57

我曾经认为在运行时创建对象将是更好的方法.我现在不太确定,因为你确实失去了一些有用的功能,尽管它可能是值得的,不管只是为了防止新手混淆.这样做的缺点是:

1.表现

def foo(arg=something_expensive_to_compute())):
    ...
Run Code Online (Sandbox Code Playgroud)

如果使用了调用时评估,则每次使用函数时都会调用昂贵的函数而不使用参数.您要么为每次调用付出昂贵的代价,要么需要在外部手动缓存该值,污染您的命名空间并添加详细程度.

2.强制绑定参数

一个有用的技巧是在创建lambda时将lambda的参数绑定到变量的当前绑定.例如:

funcs = [ lambda i=i: i for i in range(10)]
Run Code Online (Sandbox Code Playgroud)

这将返回分别返回0,1,2,3 ...的函数列表.如果行为发生了变化,它们将绑定i到i 的调用时间值,因此您将获得所有返回的函数列表9.

否则实现此方法的唯一方法是使用i绑定创建进一步的闭包,即:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]
Run Code Online (Sandbox Code Playgroud)

3.内省

考虑一下代码:

def foo(a='test', b=100, c=[]):
   print a,b,c
Run Code Online (Sandbox Code Playgroud)

我们可以使用inspect模块获取有关参数和默认值的信息

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))
Run Code Online (Sandbox Code Playgroud)

这些信息对于文档生成,元编程,装饰器等非常有用.

现在,假设可以更改默认值的行为,以便这相当于:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]
Run Code Online (Sandbox Code Playgroud)

但是,我们已经失去了内省的能力,并且看到了默认参数什么.因为没有构造对象,所以我们不能在没有实际调用函数的情况下获取它们.我们能做的最好的事情是存储源代码并将其作为字符串返回.

  • 如果每个都有一个函数来创建默认参数而不是值,那么您也可以实现自省。检查模块只会调用该函数。 (2认同)
  • @yairchu:假设构造是安全的(即没有副作用)。内省 args 不应该*做*任何事情,但评估任意代码很可能最终会产生影响。 (2认同)
  • 不同的语言设计通常只是意味着以不同的方式编写东西。你的第一个例子可以很容易地写成:_expensive = invoice(); def foo(arg=_expensive),如果你特别*不*希望它重新评估。 (2认同)

Jim*_*ard 57

你为什么不反省?

真的很惊讶,没有人执行被Python提供了精辟的内省(23上可调用适用).

给定一个简单的小函数func定义为:

>>> def func(a = []):
...    a.append(5)
Run Code Online (Sandbox Code Playgroud)

当Python遇到它时,首先要做的是编译它以便code为这个函数创建一个对象.完成此编译步骤后,Python会计算*,然后默认参数([]此处为空列表)存储在函数对象本身中.正如最佳回答所述:列表a现在可以被视为该功能的成员func.

所以,让我们做一些反省,之前和之后检查列表被如何扩大内部函数对象.我正在使用Python 3.x它,对于Python 2同样适用(使用__defaults__func_defaults在Python 2中;是的,两个名称用于相同的事情).

执行前的功能:

>>> def func(a = []):
...     a.append(5)
...     
Run Code Online (Sandbox Code Playgroud)

在Python执行此定义之后,它将采用指定的任何默认参数(a = []此处)并将它们塞入__defaults__函数对象的属性中(相关部分:Callables):

>>> func.__defaults__
([],)
Run Code Online (Sandbox Code Playgroud)

好的,所以一个空列表作为单个条目__defaults__,正如预期的那样.

执行后的功能:

我们现在执行这个功能:

>>> func()
Run Code Online (Sandbox Code Playgroud)

现在,让我们__defaults__再看一遍:

>>> func.__defaults__
([5],)
Run Code Online (Sandbox Code Playgroud)

惊讶?对象内部的值发生了变化!现在,对该函数的连续调用将简单地附加到该嵌入list对象:

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)
Run Code Online (Sandbox Code Playgroud)

所以,你有它,这个'缺陷'发生的原因是因为默认参数是函数对象的一部分.这里没有什么奇怪的事情,这一切都有点令人惊讶.

解决这个问题的常见解决方案是使用None默认值然后在函数体中初始化:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []
Run Code Online (Sandbox Code Playgroud)

由于函数体每次都重新执行,如果没有传递参数,你总是得到一个全新的空列表a.


要进一步验证列表中的列表__defaults__与函数中使用的列表相同,func您只需更改函数以返回函数体内使用id的列表a.然后,把它比作在列表中__defaults__(位置[0]__defaults__),你会看到这些确实是指的同一个列表实例:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True
Run Code Online (Sandbox Code Playgroud)

一切都具有内省的力量!


*要验证Python在编译函数期间评估默认参数,请尝试执行以下操作:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2
Run Code Online (Sandbox Code Playgroud)

正如您将注意到的那样,input()在构建函数的过程之前调用它并将其绑定到名称bar.

  • @das-g `is` 就可以了,我只是使用了 `id(val)` 因为我认为它可能更直观。 (2认同)

Lut*_*elt 53

防御Python的5分

  1. 简单性:从以下意义上说,行为很简单:大多数人只陷入一次,而不是几次.

  2. 一致性:Python 总是传递对象,而不是名称.显然,默认参数是函数标题的一部分(不是函数体).因此,应该在模块加载时评估(并且仅在模块加载时,除非嵌套),而不是在函数调用时.

  3. 实用性:正如Frederik Lundh在他对"Python中的默认参数值"的解释中指出的那样,当前的行为对于高级编程非常有用.(谨慎使用.)

  4. 足够的文档:在最基本的Python文档,教程中,该问题在"更多关于定义函数"一节的第一小节中 被大声宣布为"重要警告".警告甚至使用粗体,这很少在标题之外应用.RTFM:阅读精细手册.

  5. 元学习:陷入陷阱实际上是一个非常有用的时刻(至少如果你是一个反思学习者),因为你随后会更好地理解上面的"一致性"这一点,这将教会你很多关于Python的知识.

  • 我花了一年时间才发现这种行为搞砸了我的生产代码,最终删除了一个完整的功能,直到我偶然碰到这个设计缺陷.我正在使用Django.由于登台环境没有很多请求,因此该错误从未对QA产生任何影响.当我们上线并收到许多同时请求时 - 一些实用功能开始覆盖彼此的参数!制作安全漏洞,漏洞和什么不是. (17认同)
  • @oriadam,您的公司在拥有开发、登台和生产环境时需要代码审查和实际的专家编码人员使用他们编写的语言。新手错误和不良代码习惯不应该出现在生产代码中 (8认同)
  • @oriadam,没有冒犯,但我想知道你是如何学习Python而不是遇到这个问题的.我现在正在学习Python,这个可能的缺陷是[在官方Python教程中提到](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values)和第一次提及默认参数.(正如在这个答案的第4点中所提到的.)我认为道德是 - 而不是无情地 - 阅读用于创建制作软件的语言的官方文档**. (7认同)
  • 另外,如果除了我正在执行的函数调用之外还调用了一个复杂性未知的函数,那将是令人惊讶的(对我来说)。 (2认同)
  • 高级程序员无法神奇地捕获所有错误。代码审查非常好且必要,但不要假装它们可以防止“新手错误”进入生产环境。即使是专家也会时不时地引入此类错误。 (2认同)

ymv*_*ymv 49

这种行为很容易解释为:

  1. 函数(类等)声明只执行一次,创建所有默认值对象
  2. 一切都通过参考传递

所以:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
Run Code Online (Sandbox Code Playgroud)
  1. a 不会改变 - 每个赋值调用都会创建新的int对象 - 打印新对象
  2. b 不会更改 - 新数组是从默认值构建并打印的
  3. c 更改 - 对同一对象执行操作 - 并打印


Gle*_*ard 33

你问的是为什么这个:

def func(a=[], b = 2):
    pass
Run Code Online (Sandbox Code Playgroud)

在内部不等同于:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()
Run Code Online (Sandbox Code Playgroud)

除了显式调用func(None,None)的情况,我们将忽略它.

换句话说,为什么不存储它们中的每一个,而不是评估默认参数,并在调用函数时对它们进行评估?

答案可能就在那里 - 它会有效地将每个具有默认参数的函数转换为闭包.即使它全部隐藏在解释器中而不是一个完整的闭包,数据也必须存储在某个地方.它会更慢并且使用更多内存.

  • 没错,但它仍然会降低Python的速度,实际上这实际上是相当令人惊讶的,除非你为类定义做同样的事情,这会使它变得非常慢,因为每次实例化时都必须重新运行整个类定义类.如上所述,修复将比问题更令人惊讶. (10认同)
  • 它不需要是一个闭包 - 一个更好的方式来考虑它只会使字节码创建默认为第一行代码 - 毕竟你在那个时候编译主体 - 在代码之间没有真正的区别在正文中的参数和代码中. (5认同)
  • 现在改变它将是精神错乱 - 我们正在探索为什么它是这样的.如果它开始进行后期默认评估,则不一定会令人感到意外.毫无疑问,这样一个核心解析的差异会对整个语言产生影响,并且可能会产生许多模糊的影响. (5认同)

hyn*_*cer 31

1)所谓的"可变默认参数"问题通常是一个特殊的例子,它表明:
"所有具有此问题的函数也会受到类似实际参数的副作用问题的影响 ",
这违反了函数式编程的规则,通常是不可思考的,应该固定在一起.

例:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]
Run Code Online (Sandbox Code Playgroud)

解决方案:一个副本
的绝对安全解决方案是copydeepcopy输入,然后再去做任何与复制对象.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"
Run Code Online (Sandbox Code Playgroud)

许多内置可变类型有像拷贝的方法,some_dict.copy()或者some_set.copy(),也可以像简单的复制somelist[:]list(some_list).每个对象也可以通过copy.copy(any_object)或更彻底地复制copy.deepcopy()(如果可变对象由可变对象组成,则后者有用).有些对象基本上是基于像"文件"对象这样的副作用,并且不能通过复制有意义地再现.仿形

类似SO问题的示例问题

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]
Run Code Online (Sandbox Code Playgroud)

它不应该既不保存在此函数返回的实例的任何公共属性中.(假设私有实例的属性不应该从这个类的子类或按照惯例之外进行修改.即_var1是私有属性)

结论:
输入参数对象不应该就地修改(变异),也不应该绑定到函数返回的对象中.(如果我们优先编程没有强烈推荐的副作用.请参阅Wiki关于"副作用"(前两段在这方面是相关的.).)

2)
只有当需要对实际参数产生副作用但在默认参数上不需要时,才有用的解决方案是def ...(var1=None): if var1 is None: var1 = [] 更多..

3)在某些情况下,默认参数的可变行为很有用.

  • 是的,Python是一种具有一些功能特性的多paragigm语言.("不要因为你有锤子而使每个问题看起来像钉子.")其中许多都是Python最好的实践.Python有一个有趣的[HOWTO功能编程](https://docs.python.org/2/howto/functional.html)其他功能是闭包和currying,这里没有提到. (6认同)
  • 我希望你知道Python不是一种函数式编程语言. (5认同)
  • 我还要补充一点,在这个后期阶段,Python 的赋值语义已被明确设计为避免在必要时进行数据复制,因此副本(尤其是深层副本)的创建将对运行时和内存使用产生不利影响。因此,应仅在必要时使用它们,但新手通常很难理解何时需要。 (4认同)
  • @holdenweb 我的答案是关于其他答案中缺少的有趣内容。我希望每个读到它的人都会说:“哇,即使对于健忘的痴迷程序员,即使在更复杂的情况下,也存在解决方案,但我更喜欢......我正在做出决定,不要意外地修改参数,”您还写道另一条评论“重新绑定该名称保证它永远不会被修改。” (你的意思是重新绑定单个项目)在Python中,勤奋或复制是自由的可接受的代价,我喜欢它。 (2认同)

Ben*_*Ben 28

这实际上与默认值无关,除了它在您使用可变默认值编写函数时经常出现意外行为.

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]
Run Code Online (Sandbox Code Playgroud)

此代码中没有默认值,但您会得到完全相同的问题.

问题是,foo修改从主叫方传递一个可变变量,当主叫方不指望这个.像这样的代码如果函数被调用就好了append_5; 然后调用者将调用该函数以修改它们传入的值,并且可以预期该行为.但是这样的函数不太可能采用默认参数,并且可能不会返回列表(因为调用者已经有对该列表的引用;它刚刚传入的那个).

foo带有默认参数的原始文件不应该修改a它是显式传入还是获得默认值.您的代码应该单独保留可变参数,除非从context/name/documentation明确指出应该修改参数.使用作为参数传递的可变值作为本地临时值是一个非常糟糕的主意,无论我们是否使用Python,以及是否涉及默认参数.

如果你需要在计算某些东西时破坏性地操纵一个本地临时,并且你需要从一个参数值开始你的操作,你需要复制一份.

  • 虽然相关,但我认为这是不同的行为(正如我们所期望的那样'追加'来"改变"a"就地").在每个调用**上没有重新实例化**默认的mutable是"意外"位...至少对我而言.:) (6认同)
  • @AndyHayden如果函数是*预期*来修改参数,为什么有默认值才有意义? (2认同)
  • @AndyHayden我的答案的要点是,如果您曾经因意外改变参数的默认值而感到惊讶,那么您还有另一个错误,那就是当默认*未*使用时,您的代码可能会意外地改变调用者的值。请注意,如果参数为“None”,则使用“None”并分配真正的默认值*不能解决该问题*(因此我认为这是一种反模式)。如果您通过避免改变参数值(无论它们是否有默认值)来修复另一个错误,那么您将永远不会注意到或关心这种“令人惊讶”的行为。 (2认同)

Sté*_*ane 26

已经很忙的主题,但从我在这里读到的内容,以下内容帮助我了解它是如何在内部工作的:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232
Run Code Online (Sandbox Code Playgroud)

  • 实际上这对于新手来说可能有点混乱,因为`a = a + [1]`重载'a` ...考虑将其改为`b = a + [1]; print id(b)`并添加一行`a.append(2)`.这将使两个列表中的`+`总是创建一个新列表(分配给`b`)更明显,而修改后的`a`仍然可以具有相同的`id(a)`. (2认同)

Jas*_*ker 25

这是一种性能优化.作为此功能的结果,您认为这两个函数调用中的哪一个更快?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2
Run Code Online (Sandbox Code Playgroud)

我会给你一个提示.这是反汇编(参见http://docs.python.org/library/dis.html):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

我怀疑经验丰富的行为有实际用途(谁真的在C中使用静态变量,没有繁殖错误?)

正如你所看到的,用一成不变的默认参数时提高性能.如果它是一个经常调用的函数,或者默认参数需要很长时间来构造,这可能会有所不同.另外,请记住Python不是C.在C中你有几乎是免费的常量.在Python中你没有这个好处.


Bac*_*zek 23

最短的答案可能是"定义是执行",因此整个论证没有严格意义.作为一个更人为的例子,你可以引用这个:

def a(): return []

def b(x=a()):
    print x
Run Code Online (Sandbox Code Playgroud)

希望它足以表明在def语句执行时不执行默认参数表达式并不容易或没有意义,或两者兼而有之.

我同意当你尝试使用默认构造函数时,这是一个问题.


Aar*_*all 23

Python:Mutable默认参数

在将函数编译为函数对象时,将计算默认参数.当函数使用该函数多次时,它们是并且保持相同的对象.

当它们是可变的时,当变异时(例如,通过向其添加元素),它们在连续调用时保持变异.

他们保持变异,因为他们每次都是同一个对象.

等效代码:

由于列表在编译和实例化函数对象时绑定到函数,因此:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""
Run Code Online (Sandbox Code Playgroud)

几乎完全等同于:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding
Run Code Online (Sandbox Code Playgroud)

示范

这是一个演示 - 您可以在每次引用它们时验证它们是否是同一个对象

  • 看到在函数编译成函数对象之前创建了列表,
  • 每次引用列表时,观察id是相同的,
  • 观察当第二次调用使用它的函数时列表保持更改,
  • 观察输出源的输入顺序(我方便地为您编号):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()
Run Code Online (Sandbox Code Playgroud)

并运行它python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']
Run Code Online (Sandbox Code Playgroud)

这是否违反了"最小惊讶"的原则?

这种执行顺序经常让Python的新用户感到困惑.如果您了解Python执行模型,那么它就变得非常期待.

新Python用户的常用指令:

但这就是为什么对新用户的通常指令是创建这样的默认参数:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []
Run Code Online (Sandbox Code Playgroud)

这使用None singleton作为sentinel对象来告诉函数我们是否得到了除默认值之外的参数.如果我们没有参数,那么我们实际上想要使用一个新的空列表[],作为默认值.

正如关于控制流教程部分所说:

如果您不希望在后续调用之间共享默认值,则可以编写如下函数:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
Run Code Online (Sandbox Code Playgroud)


Dmi*_*sky 19

如果您考虑以下因素,这种行为就不足为奇了:

  1. 分配尝试时只读类属性的行为,以及
  2. 函数是对象(在接受的答案中解释得很好).

(2)的作用已在本主题中广泛涉及.(1)可能是引起惊讶的因素,因为这种行为在来自其他语言时并非"直观".

(1)关于类的Python 教程中进行了描述.尝试将值分配给只读类属性:

...在最内层范围之外找到的所有变量都是只读的(尝试写入这样的变量只会在最里面的范围内创建一个新的局部变量,保持同名的外部变量不变).

回顾原始示例并考虑以上几点:

def foo(a=[]):
    a.append(5)
    return a
Run Code Online (Sandbox Code Playgroud)

foo是一个对象,afoo(可用于foo.func_defs[0])的属性.由于a是一个列表,a是可变的,因此是一个读写属性foo.当函数被实例化时,它被初始化为由签名指定的空列表,并且只要函数对象存在,它就可用于读取和写入.

foo不覆盖默认值的情况下调用将使用该默认值foo.func_defs.在这种情况下,foo.func_defs[0]用于a函数对象的代码范围.对a更改的更改foo.func_defs[0],这是foo对象的一部分,并在执行代码之间持续存在foo.

现在,将其与模拟其他语言的默认参数行为的文档中的示例进行比较,以便每次执行函数时都使用函数签名默认值:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
Run Code Online (Sandbox Code Playgroud)

考虑到(1)(2),可以看出为什么这可以实现所需的行为:

  • foo函数对象被实例化时,foo.func_defs[0]被设置为None一个不可变对象.
  • 当使用默认值执行函数时(L在函数调用中未指定参数),foo.func_defs[0](None)在本地作用域中可用L.
  • L = [],分配无法成功foo.func_defs[0],因为该属性是只读的.
  • Per (1),在本地范围内创建一个也命名的新局部变量L,并用于函数调用的其余部分.foo.func_defs[0]因此未来的调用保持不变foo.


hug*_*o24 19

使用None的简单解决方法

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]
Run Code Online (Sandbox Code Playgroud)


Mar*_*cin 17

这里的解决方案是:

  1. 使用None作为默认值(或随机数object),以及交换机上,在运行时创建自己的价值观; 要么
  2. 使用a lambda作为默认参数,并在try块中调用它以获取默认值(这是lambda抽象的用途).

第二个选项很好,因为函数的用户可以传入一个可调用的,这可能已经存在(例如a type)


Ale*_*der 17

我将演示一个替代结构,将默认列表值传递给函数(它对字典同样有效).

正如其他人已经广泛评论的那样,list参数在定义时与函数绑定,而不是在执行时.由于列表和词典是可变的,因此对此参数的任何更改都将影响对此函数的其他调用.因此,对函数的后续调用将接收此共享列表,该列表可能已被该函数的任何其他调用更改.更糟糕的是,两个参数同时使用此函数的共享参数,而忽略了另一个参数所做的更改.

错误的方法(可能......):

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7
Run Code Online (Sandbox Code Playgroud)

您可以使用以下命令验证它们是同一个对象id:

>>> id(a)
5347866528

>>> id(b)
5347866528
Run Code Online (Sandbox Code Playgroud)

Per Brett Slatkin的"有效的Python:编写更好的Python的59种方法",第20项:使用None和文档字符串来指定动态默认参数(p.48)

在Python中实现所需结果的约定是提供默认值,None并记录docstring中的实际行为.

此实现确保对函数的每次调用都接收默认列表或传递给函数的列表.

首选方法:

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]
Run Code Online (Sandbox Code Playgroud)

可能存在"错误方法"的合法用例,其中程序员希望共享默认列表参数,但这更可能是规则之外的例外.


Sai*_*ish 16

当我们这样做时:

def foo(a=[]):
    ...
Run Code Online (Sandbox Code Playgroud)

......我们的论点分配a到一个不愿透露姓名的列表,如果主叫方没有通过的值.

为了使讨论更简单,让我们暂时给这个未命名的列表命名.怎么样pavlo

def foo(a=pavlo):
   ...
Run Code Online (Sandbox Code Playgroud)

在任何时候,如果调用者没有告诉我们什么a是,我们重用pavlo.

如果pavlo是可变的(可修改的),并foo最终修改它,我们注意到下一次foo调用的效果而不指定a.

所以这就是你所看到的(记住,pavlo初始化为[]):

 >>> foo()
 [5]
Run Code Online (Sandbox Code Playgroud)

现在,pavlo是[5].

foo()再次呼叫再次修改pavlo:

>>> foo()
[5, 5]
Run Code Online (Sandbox Code Playgroud)

指定a致电时foo(),确保pavlo没有被触及.

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]
Run Code Online (Sandbox Code Playgroud)

所以,pavlo还是[5, 5].

>>> foo()
[5, 5, 5]
Run Code Online (Sandbox Code Playgroud)


bgr*_*itl 16

我有时会利用此行为作为以下​​模式的替代方法:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()
Run Code Online (Sandbox Code Playgroud)

如果singleton仅使用use_singleton,我喜欢以下模式作为替代:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()
Run Code Online (Sandbox Code Playgroud)

我已经将它用于实例化访问外部资源的客户端类,也用于创建用于memoization的dicts或列表.

由于我不认为这种模式是众所周知的,所以我做了一个简短的评论,以防止未来的误解.

  • 我更喜欢为memoization添加一个装饰器,并将memoization缓存放在函数对象本身上. (2认同)

jdb*_*org 15

你可以通过替换对象(因此与范围的关系)来绕过这个:

def foo(a=[]):
    a = list(a)
    a.append(5)
    return a
Run Code Online (Sandbox Code Playgroud)

丑陋,但它的工作原理.

  • 如果您使用自动文档生成软件来记录函数所期望的参数类型,这是一个很好的解决方案.如果a为None,则设置a = None然后将a设置为[]无助于读者一眼就能理解预期的内容. (3认同)

Fli*_*imm 13

是的,这是Python的一个设计缺陷

我已阅读所有其他答案,但我不相信。这种设计确实违反了最小惊讶原则。

默认值可以设计为在调用函数时而不是在定义函数时进行评估。JavaScript 是这样实现的:

function foo(a=[]) {
  a.push(5);
  return a;
}
console.log(foo()); // [5]
console.log(foo()); // [5]
console.log(foo()); // [5]
Run Code Online (Sandbox Code Playgroud)

作为进一步证明这是一个设计缺陷的证据,Python 核心开发人员目前正在讨论引入新语法来解决这个问题。请参阅本文:Python 的后期绑定参数默认值

要获得更多证据表明这是一个设计缺陷,如果您在 Google 上搜索“Python 陷阱”,则该设计会被称为陷阱,通常是列表中的第一个陷阱,在前 9 个 Google 结果中(1 , 2 , 3 , 4 , 56789)。相比之下,如果你用谷歌搜索“Javascript 陷阱”,就会发现 Javascript 中默认参数的行为甚至一次都没有被视为陷阱。

根据定义,陷阱违反了最少惊讶原则。他们感到惊讶。鉴于默认参数值的行为有更好的设计,不可避免的结论是 Python 的行为代表了一个设计缺陷。

我作为一个热爱 Python 的人这么说。我们可以成为 Python 的粉丝,并且仍然承认每个对 Python 的这一方面感到不愉快的惊讶的人都是因为它一个真正的“陷阱”而感到不愉快的惊讶。

  • @LutzPrechelt 仅仅因为“def”语句在模块加载时执行,并不意味着默认的参数值也应该在模块加载时执行。采用语句 `foobar = lambda x: print("hi")` 。如果在模块加载时打印出“hi”,您会感到惊讶吗?我知道我会的,即使这个语句是在模块加载时执行的。直观上,我们期望该语句的“print”部分仅在调用 lambda 后才会执行。同样,正如人们有据可查的惊讶所证明的那样,我们期望在调用时执行默认值 (2认同)

Chr*_*ard 12

可能是这样的:

  1. 有人正在使用每种语言/库功能,并且
  2. 在这里改变行为是不明智的,但是

坚持上述两个特征完全一致,但仍然提出另一个观点:

  1. 这是一个令人困惑的功能,它在Python中是不幸的.

其他答案,或者至少其中一些答案要么分1和2而不是3分,要么分3分和低分1分和2分.但这三个都是真的.

在这里切换马匹可能会要求严重破损,并且通过更改Python以直观地处理Stefano的开放片段可能会产生更多问题.而且,熟悉Python内部人员的人可能会解释一个后果的雷区.然而,

现有的行为不是Pythonic,Python是成功的,因为很少有关于语言的内容违反了最接近这一点的最不惊讶的原则.这是一个真正的问题,无论是否根除它是明智的.这是一个设计缺陷.如果你通过试图追踪行为来更好地理解语言,我可以说C++完成所有这些以及更多; 通过导航,例如微妙的指针错误,你可以学到很多东西.但这不是Pythonic:那些关心Python足以坚持这种行为的人是那些被语言所吸引的人,因为Python比其他语言的意外要少得多.Dabblers和好奇的人成为Pythonistas,因为他们对于让事情变得有效所花费的时间感到惊讶 - 不是因为设计因素 - 我的意思是隐藏的逻辑谜题 - 削弱了被Python吸引的程序员的直觉因为它只是工作.

  • -1虽然这是一个可辩护的观点,但这不是答案,**和**我不同意它.太多特殊例外会产生他们自己的角落案例. (6认同)
  • 那么,在Python中,每次调用函数时,[]的默认参数保持[]会更有意义,这是"令人惊讶的无知". (3认同)
  • 并且无意识地将一个默认参数设置为None,然后在函数设置的主体中,如果参数== None:argument = []?考虑这个成语是不幸的,因为人们常常想要一个天真的新人所期望的,如果你指定f(argument = []),参数会自动默认为[]的值吗? (3认同)
  • 但是在Python中,语言的一部分精神是你不需要进行太多的深度潜水; 无论您对排序,大O和常量有多了解,array.sort()都能正常工作.数组排序机制中的Python之美,给出了无数个例子之一,就是你不需要深入研究内部结构.换句话说,Python的优点在于通常不需要深入了解实现以获得Just Works的功能.并且有一个解决方法(...如果参数== None:argument = []),FAIL. (3认同)
  • 作为一个独立的,语句`x = []`表示"创建一个空的列表对象,并将名称'x'绑定到它." 因此,在`def f(x = [])`中,还会创建一个空列表.它并不总是绑定到x,因此它被绑定到默认的代理.稍后当调用f()时,默认值被拖出并绑定到x.由于它是空的列表本身被松散了,所以同样的列表是唯一可用于绑定到x的内容,无论是否有任何内容被卡在其中.怎么会这样呢? (3认同)
  • 关于Python如此基础的东西,“现有行为不是Python式的”是一个令人惊讶的无知之词,并且背叛了其对象模型的悲惨的低估。 (2认同)
  • @JerryB,争论中不太令人惊讶的行为只是在每次需要默认参数时创建一个空列表对象,而不是保留相同的列表对象并每次都将其拖出。不知道为什么你说编译文本是必要的(什么文本?);当然,实际的实现可以用字节码来实现。 (2认同)

Mar*_*som 10

这不是设计缺陷.任何绊倒此事的人都在做错事.

我看到有3个案例可能会遇到这个问题:

  1. 您打算将参数修改为函数的副作用.在这种情况下,拥有默认参数永远不会有意义.唯一的例外是当您滥用参数列表以具有函数属性时,例如cache={},并且您不希望根据实际参数调用该函数.
  2. 您打算离开参数修改,但你不小心没有修改.这是一个错误,修复它.
  3. 您打算修改在函数内部使用的参数,但不希望修改在函数外部可见.在这种情况下,您需要复制参数,无论是否为默认值!Python不是一种按值调用的语言,因此它不会为您制作副本,您需要明确它.

问题中的示例可能属于类别1或3.奇怪的是,它都修改了传递的列表并返回它; 你应该选择一个或另一个.

  • 完全不同意,在很多情况下这绝对是一个设计缺陷,而不是程序员在做一些错误的事情 (5认同)
  • @MarkRansom如果我们认为副作用是可以的,那么修改默认参数作为有副作用的函数的一部分就没有什么问题。假设您有一个函数对列表执行“某些操作”并返回该列表。我们要确保该函数始终返回一个列表。那么将空(或非空)列表作为默认值就非常有意义。该语言违背了很大一部分新 Python 程序员的期望。为什么他们错了而语言是对的?如果语言有相反的行为,你会提出相反的论点吗? (2认同)
  • @MarkRansom 不,他们不是;例如,[JavaScript 没有这个设计缺陷](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters#evaluated_at_call_time)。 (2认同)

Nor*_*ldt 9

这个"虫子"给了我很多加班时间!但我开始看到它的潜在用途(但我还是喜欢它在执行时,仍然)

我会给你我看到的一个有用的例子.

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()
Run Code Online (Sandbox Code Playgroud)

打印以下内容

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way
Run Code Online (Sandbox Code Playgroud)

  • 你的例子看起来不太现实。为什么要将“errors”作为参数传递,而不是每次都从头开始? (2认同)

ytp*_*lai 8

只需将功能更改为:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
Run Code Online (Sandbox Code Playgroud)


Prz*_*k D 8

每个其他答案都解释了为什么这实际上是一个很好的和期望的行为,或者为什么你不应该需要它。我是为那些想要行使他们的权利使语言服从他们的意愿的顽固的人,而不是相反的。

我们将使用一个装饰器来“修复”这种行为,该装饰器将复制默认值,而不是为每个保留其默认值的位置参数重用相同的实例。

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper
Run Code Online (Sandbox Code Playgroud)

现在让我们使用这个装饰器重新定义我们的函数:

@sanify
def foo(a=[]):
    a.append(5)
    return a

foo() # '[5]'
foo() # '[5]' -- as desired
Run Code Online (Sandbox Code Playgroud)

这对于带有多个参数的函数来说特别简洁。相比:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work
Run Code Online (Sandbox Code Playgroud)

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!
Run Code Online (Sandbox Code Playgroud)

重要的是要注意,如果您尝试使用关键字 args,上述解决方案会中断,如下所示:

foo(a=[4])
Run Code Online (Sandbox Code Playgroud)

可以调整装饰器以实现这一点,但我们将此作为练习留给读者;)


use*_*994 7

我认为这个问题的答案在于python如何将数据传递给参数(通过值或通过引用传递),而不是可变性或python如何处理"def"语句.

简要介绍.首先,python中有两种类型的数据类型,一种是简单的基本数据类型,如数字,另一种数据类型是对象.其次,当数据传递给参数时,python按值传递基本数据类型,即将值的本地副本设置为局部变量,但是通过引用传递对象,即指向对象的指针.

承认以上两点,让我们解释一下python代码发生了什么.它只是因为通过对象的引用传递,而与可变/不可变无关,或者可以说是"def"语句在定义时只执行一次这一事实.

[]是一个对象,所以python传递[]的引用a,即a只是一个指向[]的指针,它作为一个对象位于内存中.然而,只有一个[]的副本,但有很多引用它.对于第一个foo(),list [] 通过append方法更改为1.但请注意,列表对象只有一个副本,此对象现在变为1.当运行第二个foo()时,effbot网页所说的内容(不再评估项目)是错误的.a被评估为列表对象,尽管现在对象的内容是1.这是通过引用传递的效果!foo(3)的结果可以以相同的方式容易地导出.

为了进一步验证我的答案,让我们看看另外两个代码.

======第2号========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]
Run Code Online (Sandbox Code Playgroud)

[]是一个对象,所以None(前者是可变的而后者是不可变的.但是可变性与问题无关).没有在空间的某个地方,但我们知道它在那里,那里只有一个无副本.因此,每次调用foo时,都会评估项目(而不是仅评估一次的某些答案)为None,要清楚,是None的引用(或地址).然后在foo中,item被改为[],即指向另一个具有不同地址的对象.

======第3号=======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]
Run Code Online (Sandbox Code Playgroud)

调用foo(1)使项目指向带有地址的列表对象[],例如11111111.列表的内容在续集中的foo函数中更改为1,但地址未更改,仍然是11111111然后foo(2,[])即将来临.虽然foo(2,[])中的[]在调用foo(1)时具有与默认参数[]相同的内容,但它们的地址是不同的!由于我们明确提供参数,items必须取这个新的地址[],比如说2222222,并在做了一些更改后返回它.现在执行foo(3).由于仅x提供,因此项目必须再次采用其默认值.什么是默认值?它在定义foo函数时设置:列表对象位于11111111.因此,项目被评估为具有元素1的地址11111111.位于2222222的列表也包含一个元素2,但它没有指向任何项目更多.因此,3的附加将items[1,3].

从上面的解释中,我们可以看到在接受的答案中推荐的effbot网页未能给出这个问题的相关答案.更重要的是,我认为在effbot网页中有一点是错误的.我认为关于UI.Button的代码是正确的:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)
Run Code Online (Sandbox Code Playgroud)

每个按钮可以保存一个独特的回调函数,它将显示不同的值i.我可以提供一个示例来说明这一点:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 
Run Code Online (Sandbox Code Playgroud)

如果我们执行,x[7]()我们将按预期获得7,x[9]()并将给出9,另一个值为i.

  • 你的最后一点是错的.尝试一下,你会看到`x [7]()`是`9`. (5认同)
  • “ python按值传递基本数据类型,即,将值的本地副本复制到本地变量”是完全错误的。令我惊讶的是,有人显然可以很好地了解Python,却对基本原理有如此可怕的误解。:-( (2认同)

Mis*_*agi 6

TLDR:定义时间默认值是一致的,并且更具表现力。


定义一个函数影响两个范围:该范围定义包含的功能,并执行范围由包含的功能。尽管很清楚块是如​​何映射到作用域的,但问题是在哪里def <name>(<args=defaults>):属于:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope
Run Code Online (Sandbox Code Playgroud)

def name零件必须在定义范围内进行评估- name毕竟我们希望在那里可用。仅在内部评估函数将使其无法访问。

由于parameter是一个常量名,因此我们可以与同时“评估”它def name。这还有一个优势,那就是它可以生成具有已知签名的功能name(parameter=...):,而不是裸露的签名name(...):

现在,什么时候评估default

一致性已经说了“在定义时”:def <name>(<args=defaults>):在定义时最好也评估其他所有内容。延迟其中的一部分将是令人惊讶的选择。

两种选择都不相等:如果default在定义时求值,它仍然会影响执行时间。如果default在执行时评估,则不会影响定义时间。选择“在定义时”允许表达两种情况,而选择“在执行时”只能表达一种情况:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...
Run Code Online (Sandbox Code Playgroud)

  • “一致性已经说过“在定义时”:`def &lt;name&gt;(&lt;args=defaults&gt;):`的其他所有内容也最好在定义时进行评估。” 我不认为从前提得出结论。仅仅因为两个事物位于同一行并不意味着它们应该在同一范围内进行评估。`default` 与该行的其余部分不同:它是一个表达式。计算表达式与定义函数是一个非常不同的过程。 (2认同)
  • 好的,创建一个函数在某种意义上意味着求值,但显然不是在定义时对其中的每个表达式求值的意义上。大多数都不是。我不清楚在定义时签名在什么意义上被特别“评估”,就像函数体被“评估”(解析成合适的表示)一样;而函数体中的表达式显然没有得到完整意义上的计算。从这个角度来看,一致性意味着签名中的表达式也不应该被“完全”评估。 (2认同)
  • 我并不是说你错了,只是你的结论不能仅从一致性得出。 (2认同)

归档时间:

查看次数:

143279 次

最近记录:

6 年,4 月 前