ChatGPT解决这个技术问题 Extra ChatGPT

“Least Astonishment”和可变默认参数

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

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

Python 新手会期望这个不带参数调用的函数总是返回一个只有一个元素的列表:[5]。结果却大不相同,而且非常惊人(对于新手来说):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

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

编辑:

Baczek made an interesting example。连同您的大部分评论和Utaal's in particular,我进一步阐述:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

对我来说,设计决策似乎与将参数范围放在哪里有关:在函数内部,还是与它“一起”?

在函数内部进行绑定意味着 x 在函数被调用时有效地绑定到指定的默认值,而不是定义,这会带来一个严重的缺陷:def 行将是“混合”的,即(函数对象的)绑定的一部分将在定义时发生,而部分(默认参数的分配)将在函数调用时发生。

实际行为更加一致:该行的所有内容都在执行该行时进行评估,这意味着在函数定义处。

我毫不怀疑可变参数违反了普通人的最小惊讶原则,而且我看到初学者走到那里,然后英勇地用邮件元组替换邮件列表。尽管如此,可变参数仍然符合 Python Zen (Pep 20) 并且属于“明显的荷兰语”(由核心 Python 程序员理解/利用)条款。建议使用 doc string 的解决方法是最好的,但现在对 doc strings 和任何(书面)文档的抵抗力并不少见。就个人而言,我更喜欢装饰器(比如@fixed_defaults)。
当我遇到这个问题时,我的论点是:“为什么你需要创建一个函数来返回一个可变的,它可以是一个你可以传递给函数的可变变量?它要么改变一个可变的,要么创建一个新的。你为什么需要用一个函数来做这两个?为什么要重写解释器以允许你在不添加三行代码的情况下做到这一点?因为我们在这里讨论的是重写解释器处理函数定义和调用的方式。对于一个几乎不需要的用例来说,这需要做很多事情。
“Python 新手会期望这个函数总是返回一个只有一个元素的列表:[5]。”我是一名 Python 新手,我不希望这样,因为显然 foo([1]) 将返回 [1, 5],而不是 [5]。你的意思是新手会期望函数无参数调用总是返回[5]
这个问题问“为什么这个[错误的方式]被这样实现了?”它没有问“什么是正确的方式?”< /i>,由 [Why does using arg=None fix Python's mutable default argument issue?]*(stackoverflow.com/questions/10676729/…) 介绍。新用户几乎总是对前者不太感兴趣,而对后者更感兴趣,所以有时这是一个非常有用的链接/欺骗引用。

m
martineau

实际上,这不是设计缺陷,也不是因为内部或性能。这仅仅是因为 Python 中的函数是一流的对象,而不仅仅是一段代码。

只要你这样想,那么它就完全有道理了:函数是根据其定义进行评估的对象;默认参数是一种“成员数据”,因此它们的状态可能会从一个调用更改为另一个调用 - 就像在任何其他对象中一样。

无论如何,effbot (Fredrik Lundh) 对 Default Parameter Values in Python 中这种行为的原因有一个很好的解释。我发现它非常清楚,我真的建议阅读它以更好地了解函数对象的工作原理。


对于阅读上述答案的任何人,我强烈建议您花时间阅读链接的 Effbot 文章。除了所有其他有用的信息外,关于如何将此语言功能用于结果缓存/记忆的部分非常容易了解!
即使它是一流的对象,人们仍然可以设想这样一种设计,其中每个默认值的代码与对象一起存储并在每次调用函数时重新评估。我并不是说这会更好,只是函数是一流的对象并不能完全排除它。
抱歉,任何被认为是“Python 中最大的 WTF”的东西绝对是一个设计缺陷。在某些时候,这对每个人来说都是错误的来源,因为没有人在一开始就期望这种行为 - 这意味着它不应该从一开始就被设计成那样。我不在乎他们必须跳过什么箍,他们应该设计 Python,以便默认参数是非静态的。
无论这是否是设计缺陷,您的回答似乎暗示这种行为在某种程度上是必要的、自然和明显的,因为函数是一流的对象,而事实并非如此。 Python 有闭包。如果将默认参数替换为函数第一行的赋值,它会在每次调用时评估表达式(可能使用在封闭范围中声明的名称)。完全没有理由每次以完全相同的方式调用函数时都评估默认参数是不可能或不合理的。
该设计并非直接沿用 functions are objects。在您的范例中,建议是将函数的默认值作为属性而不是属性来实现。
J
Josh Correia

假设您有以下代码

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

def eat(food=fruits):
    ...

当我看到eat的声明时,最不奇怪的是认为如果第一个参数没有给出,它会等于元组("apples", "bananas", "loganberries")

但是,假设稍后在代码中,我会执行类似的操作

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

那么如果默认参数是在函数执行而不是函数声明时绑定的,我会惊讶地(以一种非常糟糕的方式)发现水果已经改变了。这比发现上面的 foo 函数正在改变列表更令人惊讶。

真正的问题在于可变变量,所有语言都在一定程度上存在这个问题。这是一个问题:假设在 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?

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

您的示例是 Python 新手会感到惊讶和被咬的一个很好的例子。但我认为,如果我们“解决”这个问题,那只会造成一种不同的情况,他们会被咬伤,而且这种情况会更不直观。此外,在处理可变变量时总是如此;您总是会遇到这样的情况,即有人可以根据他们正在编写的代码直观地期待一种或相反的行为。

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


我认为这是一个有争议的问题。您正在对全局变量进行操作。在代码中任何地方执行的涉及全局变量的任何评估现在都将(正确地)引用(“blueberries”,“mangos”)。默认参数可能与任何其他情况一样。
实际上,我认为我不同意您的第一个示例。我不确定我是否喜欢首先像这样修改初始化程序的想法,但如果我这样做了,我希望它的行为与您描述的完全一样——将默认值更改为 ("blueberries", "mangos")
默认参数与任何其他情况一样。出乎意料的是该参数是全局变量,而不是局部变量。这又是因为代码在函数定义处执行,而不是调用。一旦你明白了,对于课程也是如此,那就非常清楚了。
我发现这个例子具有误导性而不是出色。如果 some_random_function() 附加到 fruits 而不是分配给它,eat() 的行为 改变。就目前的精彩设计而言。如果您使用在其他地方引用的默认参数,然后从函数外部修改引用,您就是在自找麻烦。真正的 WTF 是当人们定义一个新的默认参数(列表文字或对构造函数的调用),并且 still 得到一点。
您只是显式声明了 global 并重新分配了元组 - 如果 eat 之后的工作方式不同,这绝对没有什么令人惊讶的。
B
Boris Verkhovskiy

documentation 的相关部分:

执行函数定义时,从左到右计算默认参数值。这意味着表达式在定义函数时被计算一次,并且每次调用都使用相同的“预计算”值。当默认参数是可变对象(例如列表或字典)时,这一点尤其重要:如果函数修改了对象(例如,通过将项目附加到列表中),则默认值实际上已被修改。这通常不是预期的。解决这个问题的一种方法是使用 None 作为默认值,并在函数体中显式测试它,例如: def whats_on_the_telly(penguin=None): if penguin is None: penguin = [] penguin.append("property of动物园”)归来企鹅


“这通常不是预期的”和“解决这个问题的方法”这些短语闻起来像是在记录设计缺陷。
@bukzor:需要注意和记录陷阱,这就是为什么这个问题很好并且得到了如此多的支持。同时,陷阱不一定需要消除。有多少 Python 初学者将一个列表传递给一个修改它的函数,并且惊讶地看到原始变量中出现的变化?然而,当您了解如何使用可变对象类型时,它们非常棒。我想这归结为对这个特殊陷阱的看法。
短语“这通常不是预期的”意味着“不是程序员真正想要发生的事情”,而不是“不是 Python 应该做的事情”。
@holdenweb 哇,我迟到了。鉴于上下文,bukzor 是完全正确的:当他们决定语言应该执行函数的定义时,他们正在记录并非“有意”的行为/后果。由于这是他们设计选择的意外结果,因此这是一个设计缺陷。如果这不是设计缺陷,甚至没有必要提供“解决这个问题的方法”。
我们可以用它来聊天并讨论它还有什么可能,但语义已经被彻底辩论过,没有人能想出一个明智的机制来创建默认值-on-call。一个严重的问题是,调用范围通常与定义完全不同,如果在调用时评估默认值,则名称解析不确定。 “绕过”意味着“您可以通过以下方式实现您想要的目的”,而不是“这是 Python 设计中的错误”。
G
Georgy

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

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

a = []

您希望获得 a 引用的列表。

为什么要在 a=[]

def x(a=[]):

在函数定义而不是调用时实例化一个新列表?就像您在问“如果用户不提供参数然后实例化一个新列表并使用它,就好像它是由调用者生成的一样”。我认为这是模棱两可的:

def x(a=datetime.datetime.now()):

用户,您希望 a 默认为您定义或执行 x 时对应的日期时间吗?在这种情况下,与前一个一样,我将保持相同的行为,就好像默认参数“赋值”是函数的第一条指令(在函数调用时调用 datetime.now())。另一方面,如果用户想要定义时间映射,他可以写:

b = datetime.datetime.now()
def x(a=b):

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

def x(static a=b):

你可以这样做: def x(a=None): 然后,如果 a 是 None,设置 a=datetime.datetime.now()
这次真是万分感谢。我真的无法解释为什么这让我感到无休止。你用最少的模糊和混乱做得很好。作为一个来自 C++ 系统编程并且有时天真地“翻译”语言特性的人,这个虚假的朋友让我大吃一惊,就像类属性一样。我明白事情为什么会这样,但我不禁不喜欢它,不管它会带来什么积极的影响。至少它与我的经验如此相反,我可能(希望)永远不会忘记它......
@Andreas 一旦你使用 Python 足够长的时间,你就会开始看到 Python 以它的方式将事物解释为类属性是多么合乎逻辑——这仅仅是因为 C++(和 Java 等语言)的特殊怪癖和限制,以及C#...) 将 class {} 块的内容解释为属于 instances 是有意义的 :) 但是当类是一等对象时,很自然的事情是它们的内容(在内存中)以反映它们的内容(在代码中)。
在我的书中,规范结构不是怪癖或限制。我知道它可能笨拙和丑陋,但你可以称它为某事物的“定义”。动态语言在我看来有点像无政府主义者:当然每个人都是自由的,但你需要结构来让某人清空垃圾并铺平道路。估计我老了……:)
函数定义在模块加载时执行。函数体在函数调用时执行。默认参数是函数定义的一部分,而不是函数体。 (嵌套函数变得更加复杂。)
L
Lennart Regebro

嗯,原因很简单,绑定是在代码执行时完成的,并且函数定义也被执行,嗯……当函数被定义时。

比较一下:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

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

这只是“它是如何工作的”,并且在函数案例中使其以不同的方式工作可能会很复杂,而在类案例中可能是不可能的,或者至少会大大减慢对象实例化的速度,因为您必须保留类代码并在创建对象时执行它。

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

也就是说,它应该在任何优秀的 Python 教程中占据显着位置。因为正如你所说,每个人迟早都会遇到这个问题。


您如何为类的每个实例定义不同的类属性?
如果每个实例都不同,则它不是类属性。类属性是 CLASS 上的属性。由此得名。因此,它们对于所有实例都是相同的。
你如何在一个类中定义一个类的每个实例都不同的属性? (为那些无法确定不熟悉 Python 命名约定的人可能会询问类的普通成员变量的人重新定义)。
@Kievieli:你在谈论一个类的普通成员变量。 :-) 您可以通过在任何方法中说 self.attribute = value 来定义实例属性。例如 __init__()。
@Kieveli:两个答案:您不能,因为您在类级别定义的任何东西都将是类属性,并且访问该属性的任何实例都将访问相同的类属性;你可以,/sort of/,通过使用 propertys - 这实际上是类级别的函数,它们的作用类似于普通属性,但将属性保存在实例而不是类中(通过使用 Lennart 所说的 self.attribute = value)。
T
Toby Speight

为什么不反省?

真的很惊讶没有人对可调用对象执行 Python(23 应用)提供的富有洞察力的内省。

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

>>> def func(a = []):
...    a.append(5)

当 Python 遇到它时,它会做的第一件事就是编译它,以便为这个函数创建一个 code 对象。完成此编译步骤后,Python 评估*,然后存储默认参数(此处为空列表 [])在函数对象本身中。正如上面提到的答案:列表 a 现在可以被视为函数 func成员

所以,让我们做一些自省,检查列表是如何在函数对象内部扩展的。我为此使用 Python 3.x,对于 Python 2 同样适用(在 Python 2 中使用 __defaults__func_defaults;是的,同一事物的两个名称)。

执行前的功能:

>>> def func(a = []):
...     a.append(5)
...     

在 Python 执行此定义后,它将采用指定的任何默认参数(此处为 a = [])和 cram them in the __defaults__ attribute for the function object(相关部分:Callables):

>>> func.__defaults__
([],)

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

执行后的功能:

现在让我们执行这个函数:

>>> func()

现在,让我们再看看那些 __defaults__

>>> func.__defaults__
([5],)

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

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

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

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

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

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

要进一步验证 __defaults__ 中的列表是否与函数 func 中使用的列表相同,您只需更改函数以返回函数体内使用的列表 aid。然后,将它与 __defaults__ 中的列表(__defaults__ 中的位置 [0])进行比较,您将看到它们实际上是如何引用同一个列表实例的:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

都具有内省的力量!

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

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

您会注意到,在构建函数并将其绑定到名称 bar 之前调用了 input()


最后一次验证是否需要 id(...),或者 is 运算符会回答相同的问题吗?
@das-g is 会很好,我只是使用 id(val) 因为我认为它可能更直观。
使用 None 作为默认值严重限制了 __defaults__ 内省的有用性,因此我认为这不能很好地保护 __defaults__ 以它的方式工作。惰性评估将做更多的事情来保持函数默认值对双方都有用。
B
Brian

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

1. 性能

def foo(arg=something_expensive_to_compute())):
    ...

如果使用调用时评估,那么每次使用函数时都会调用昂贵的函数而不带参数。您要么为每次调用付出昂贵的代价,要么需要在外部手动缓存值,从而污染您的命名空间并增加冗长性。

2.强制绑定参数

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

funcs = [ lambda i=i: i for i in range(10)]

这将返回一个函数列表,这些函数分别返回 0,1,2,3...。如果行为发生变化,它们会将 i 绑定到 i 的 call-time 值,因此您将获得所有返回 9 的函数列表。

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

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. 内省

考虑代码:

def foo(a='test', b=100, c=[]):
   print a,b,c

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

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

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

现在,假设可以更改默认值的行为,使其等效于:

_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=[]

然而,我们已经失去了反省的能力,看看默认参数是什么。因为对象还没有被构造,所以如果不实际调用函数,我们就永远无法获得它们。我们能做的最好的事情是存储源代码并将其作为字符串返回。


如果每个函数都有一个创建默认参数而不是值的函数,您也可以实现自省。检查模块只会调用该函数。
@SilentGhost:我说的是是否更改了行为以重新创建它 - 创建一次是当前行为,以及为什么存在可变默认问题。
@yairchu:假设构造是安全的(即没有副作用)。反省 args 不应该做任何事情,但评估任意代码很可能最终会产生影响。
不同的语言设计通常意味着以不同的方式编写东西。你的第一个例子可以很容易地写成: def foo(arg=_expensive),如果您特别不想重新评估它。
@Glenn - 这就是我所说的“在外部缓存变量” - 它有点冗长,但是你最终会在你的命名空间中得到额外的变量。
j
jakubde

5分防守Python

简单:行为在以下意义上很简单:大多数人只掉入这个陷阱一次,而不是几次。一致性:Python 总是传递对象,而不是名称。显然,默认参数是函数标题的一部分(不是函数体)。因此,它应该在模块加载时进行评估(并且仅在模块加载时,除非嵌套),而不是在函数调用时。有用性:正如 Frederik Lundh 在他对“Python 中的默认参数值”的解释中指出的那样,当前行为对于高级编程非常有用。 (谨慎使用。) 足够的文档:在最基本的 Python 文档中,本教程中,该问题在“更多关于定义函数”部分的第一小节中被大声宣布为“重要警告”。警告甚至使用粗体字,这很少应用在标题之外。 RTFM:阅读精美的手册。元学习:陷入陷阱实际上是一个非常有帮助的时刻(至少如果你是一个反思型学习者),因为你随后会更好地理解上面的“一致性”这一点,这将教会你很多关于 Python 的知识。


我花了一年的时间才发现这种行为会弄乱我的生产代码,最终删除了一个完整的功能,直到我偶然遇到了这个设计缺陷。我正在使用 Django。由于暂存环境没有很多请求,因此此错误从未对 QA 产生任何影响。当我们上线并同时收到许多请求时 - 一些实用程序函数开始覆盖彼此的参数!制造安全漏洞、错误等等。
@oriadam,无意冒犯,但我想知道您是如何学习 Python 而没有遇到过这个问题的。我现在正在学习 Python,这个可能的陷阱是 mentioned in the official Python tutorial 就在第一次提到默认参数的同时。 (如本答案的第 4 点所述。)我认为道德是阅读您用于创建生产软件的语言的官方文档,而不是同情。
此外,如果除了我正在进行的函数调用之外还调用了一个未知复杂性的函数,那将是令人惊讶的(对我来说)。
@oriadam,您的公司需要在开发、登台和生产环境时使用他们编写的语言进行代码审查和实际的专家编码人员。新手错误和不良代码习惯不应该出现在生产代码中
D
Dimitris Fasarakis Hilliard

这种行为很容易解释为:

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

所以:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c

a 不变 - 每个赋值调用都会创建新的 int 对象 - 打印新对象 b 不变 - 从默认值构建新数组并打印 c 更改 - 对同一对象执行操作 - 并打印


(实际上,add 是一个不好的例子,但整数不可变仍然是我的主要观点。)
在检查后意识到这一点让我很懊恼,当 b 设置为 [] 时,b.__add__([1]) 返回 [1],但即使列表是可变的,b 仍然是 []。我的错。
@ANon:有 __iadd__,但它不适用于 int。当然。 :-)
C
Community

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]

解决方案复制
绝对安全的解决方案是copydeepcopy输入首先对象,然后对副本执行任何操作。

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

许多内置的可变类型都有像 some_dict.copy()some_set.copy() 这样的复制方法,或者可以像 somelist[:]list(some_list) 这样轻松复制。每个对象也可以通过 copy.copy(any_object) 或更彻底地通过 copy.deepcopy() 复制(如果可变对象由可变对象组成,则后者很有用)。有些对象基本上是基于像“文件”对象这样的副作用,并且不能通过副本进行有意义的复制。 copying

a similar SO question 的示例问题

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]

它不应保存在此函数返回的实例的任何 public 属性中。 (假设实例的 private 属性不应按照约定从此类或子类外部修改。即 _var1 是私有属性)

结论:
输入参数对象不应就地修改(变异),也不应绑定到函数返回的对象中。 (如果我们更喜欢没有副作用的编程,强烈推荐。请参阅 Wiki about "side effect"(前两段与此上下文相关。)。)

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

3) 在某些情况下是the mutable behavior of default parameters useful


我希望您知道 Python 不是一种函数式编程语言。
是的,Python 是一种具有一些功能特性的多范式语言。 (“不要仅仅因为你有一把锤子就让每个问题看起来都像钉子。”)其中许多都是 Python 的最佳实践。 Python 有一个有趣的 HOWTO Functional Programming 其他特性是闭包和柯里化,这里没有提到。
我还要补充一点,在这个后期阶段,Python 的赋值语义已被明确设计为在必要时避免数据复制,因此创建副本(尤其是深度副本)将对运行时和内存使用产生不利影响。因此,它们只应在必要时使用,但新手通常难以理解何时使用。
@holdenweb 我同意。临时副本是最常用的方法,有时也是保护原始可变数据免受可能修改它们的无关函数的唯一可能方法。幸运的是,一个不合理地修改数据的函数被认为是一个错误,因此并不常见。
我同意这个答案。而且我不明白为什么当您真正的意思是其他意思时建议使用 def f( a = None ) 构造。复制是可以的,因为你不应该改变论点。当您执行 if a is None: a = [1, 2, 3] 时,无论如何您都会复制该列表。
G
Glenn Maynard

你要问的是为什么:

def func(a=[], b = 2):
    pass

在内部不等价于:

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()

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

换句话说,与其评估默认参数,为什么不存储它们中的每一个,并在调用函数时评估它们呢?

一个答案可能就在那里——它可以有效地将每个带有默认参数的函数变成一个闭包。即使它全部隐藏在解释器中而不是完全关闭,数据也必须存储在某个地方。它会更慢并使用更多内存。


它不需要是一个闭包——一种更好的思考方式是让字节码创建默认值作为第一行代码——毕竟你在那一点上编译了主体——代码之间没有真正的区别在正文中的参数和代码中。
是的,但它仍然会减慢 Python 的速度,而且实际上会非常令人惊讶,除非你对类定义做同样的事情,这会使它变得非常慢,因为你每次实例化一个类时都必须重新运行整个类定义班级。如前所述,修复将比问题更令人惊讶。
同意伦纳特。正如 Guido 喜欢说的那样,对于每一种语言特性或标准库,都有人在使用它。
现在改变它会很疯狂——我们只是在探索它为什么会这样。如果它一开始就进行了后期默认评估,那并不一定令人惊讶。毫无疑问,这样一个核心的解析差异会对整个语言产生广泛的影响,而且可能会产生许多模糊的影响。
B
Ben

这实际上与默认值无关,只是当您编写具有可变默认值的函数时,它通常会出现意外行为。

>>> 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]

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

问题是 foo 正在修改一个从调用者传入的可变变量,而调用者并不期望这样做。如果函数被称为 append_5; 这样的代码就可以了。那么调用者将调用该函数以修改它们传入的值,并且该行为将是预期的。但是这样的函数不太可能采用默认参数,并且可能不会返回列表(因为调用者已经拥有对该列表的引用;它刚刚传入的那个)。

您的原始 foo(带有默认参数)不应修改 a,无论它是显式传入还是获得默认值。您的代码应该单独保留可变参数,除非从上下文/名称/文档中清楚地知道参数应该被修改。使用作为参数传入的可变值作为本地临时变量是一个非常糟糕的主意,无论我们是否使用 Python 以及是否涉及默认参数。

如果您需要在计算过程中破坏性地操作本地临时变量,并且需要从参数值开始操作,则需要制作副本。


尽管相关,但我认为这是不同的行为(因为我们期望 append 会“就地”更改 a)。 默认可变变量不会在每次调用时重新实例化是“意外”位......至少对我而言。 :)
@AndyHayden 如果希望该函数修改参数,为什么设置默认值有意义?
@AndyHayden 我在这里留下了自己的答案,并扩展了这种情绪。让我知道你的想法。为了完整起见,我可能会将您的 cache={} 示例添加到其中。
@AndyHayden 我的回答的重点是,如果您曾因意外更改参数的默认值而感到惊讶,那么您还有另一个错误,即您的代码可能会在默认 不存在时意外更改调用者的值 使用。请注意,如果 arg 为 None,则使用 None 并分配真正的默认值 不能解决该问题(因此,我认为它是一种反模式)。如果您通过避免改变参数值(无论它们是否具有默认值)来修复另一个错误,那么您将永远不会注意到或关心这种“令人惊讶的”行为。
@AndyHayden虽然这是微妙的事情,但如果您描述构造函数的调用者提供一个值而不是使用默认值,会发生什么?现在您已经将对象的内部属性别名为调用者拥有的外部值!这类事情是难以追踪的错误的丰富来源;这几乎比意外存储默认值然后对其进行变异更糟糕(因为如果您多次实例化该类,至少应该很快就会显示出令人头疼的行为)。
R
Russia Must Remove Putin

Python:可变的默认参数

默认参数在函数被编译成函数对象时被评估。当被函数使用时,被该函数多次使用时,它们是并且仍然是同一个对象。

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

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

等效代码:

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

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

几乎完全等同于:

_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

示范

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

看到列表是在函数完成编译为函数对象之前创建的,

观察每次引用列表时 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()

并使用 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']

这是否违反“最小惊讶”原则?

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

给 Python 新用户的常用说明:

但这就是为什么对新用户的通常指示是创建他们的默认参数,而不是这样:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

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

正如 tutorial section on control flow 所说:

如果您不希望在后续调用之间共享默认值,则可以改为这样编写函数: def f(a, L=None): if L is None: L = [] L.append(a) return大号


A
Ashraf.Shk786

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

def a(): return []

def b(x=a()):
    print x

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

不过,我同意当您尝试使用默认构造函数时这是一个问题。


S
Stéphane

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

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

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

这是一个性能优化。由于此功能,您认为这两个函数调用中哪个更快?

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

print_tuple()        #1
print_tuple((1,2,3)) #2

我给你一个提示。下面是反汇编(参见 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

#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

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

如您所见,使用不可变的默认参数时有性能优势。如果它是一个经常调用的函数或默认参数需要很长时间来构建,这可能会有所不同。另外,请记住 Python 不是 C。在 C 中,您有几乎免费的常量。在 Python 中你没有这个好处。


H
HoldOffHunger

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

赋值尝试时只读类属性的行为,并且函数是对象(在接受的答案中得到了很好的解释)。

(2) 的作用已在此线程中广泛介绍。 (1) 可能是造成惊讶的因素,因为这种行为在来自其他语言时不是“直观的”。

(1) 在 Python tutorial on classes 中进行了描述。尝试将值分配给只读类属性:

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

回顾一下原来的例子,考虑以上几点:

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

这里 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 中的代码执行之间持续存在。

现在,将其与 emulating the default argument behavior of other languages 上的文档中的示例进行比较,这样每次执行函数时都会使用函数签名默认值:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

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

当 foo 函数对象被实例化时,foo.func_defs[0] 被设置为 None,一个不可变的对象。

当函数以默认值执行时(在函数调用中没有为 L 指定参数), foo.func_defs[0] (None) 在本地范围内作为 L 可用。

在 L = [] 时,分配不能在 foo.func_defs[0] 处成功,因为该属性是只读的。

根据 (1),在本地范围内创建了一个也称为 L 的新局部变量,并用于函数调用的其余部分。 foo.func_defs[0] 因此对于 foo 的未来调用保持不变。


C
Christos Hayward

这可能是真的:

有人在使用每种语言/库功能,在这里切换行为是不明智的,但是

坚持上述两个特征并提出另一点是完全一致的:

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

其他答案,或者至少其中一些答案要么提出第 1 点和第 2 点,但没有提出第 3 点,或者提出第 3 点并淡化第 1 和第 2 点。但所有三个都是正确的。

在这里中途换马可能会要求重大破坏,并且通过更改 Python 以直观地处理 Stefano 的开头片段可能会产生更多问题。一个熟悉 Python 内部原理的人可能会解释一个后果雷区,这可能是真的。然而,

现有的行为不是 Pythonic,Python 之所以成功,是因为该语言几乎没有严重违反最小惊讶原则。这是一个真正的问题,是否将其连根拔起是否明智。这是一个设计缺陷。如果您通过尝试追踪行为来更好地理解语言,我可以说 C++ 完成了所有这些以及更多;例如,通过导航细微的指针错误,您可以学到很多东西。但这不是 Pythonic:那些关心 Python 足以在这种行为面前坚持不懈的人是被该语言所吸引的人,因为 Python 比其他语言少得多的惊喜。涉猎者和好奇者会在惊讶于让某件事情工作所需的时间如此之短而感到惊讶时——不是因为设计——我的意思是,隐藏的逻辑难题——这违背了被 Python 吸引的程序员的直觉因为它只是工作。


-1 虽然是一个站得住脚的观点,但这不是一个答案,我不同意。太多的特殊例外会产生它们自己的极端情况。
那么,说在 Python 中每次调用函数时保留 [] 的默认参数更有意义是“令人惊讶的无知”吗?
并且将默认参数设置为None,然后在函数体的主体中设置默认参数是无知的,如果argument == None:argument = []?认为这个习语很不幸,因为人们经常想要一个天真的新手所期望的,如果你分配 f(argument = []),argument 将自动默认为 [] 的值,这是无知的吗?
但在 Python 中,该语言的部分精神在于您不必进行太多深入研究;无论您对排序、大 O 和常量了解多少,array.sort() 都可以正常工作。 Python 在数组排序机制中的美妙之处,举一个无数的例子,就是你不需要深入研究内部结构。换一种说法,Python 的美妙之处在于,通常不需要深入研究实现来获得 Just Works 的东西。并且有一个解决方法(...如果参数 == 无:参数 = []),失败。
作为一个独立的语句,x=[] 表示“创建一个空列表对象,并将名称‘x’绑定到它。”因此,在 def f(x=[]) 中,还创建了一个空列表。它并不总是绑定到 x,而是绑定到默认代理项。稍后当 f() 被调用时,默认值被拖出并绑定到 x。因为它是空列表本身被松散了,所以同一个列表是唯一可以绑定到 x 的东西,无论是否有任何东西被卡在里面。不然怎么可能?
h
hugo24

使用无的简单解决方法

>>> 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]

这不是问题的答案。
A
Alexander

我将演示一种将默认列表值传递给函数的替代结构(它同样适用于字典)。

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

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

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

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

>>> id(a)
5347866528

>>> id(b)
5347866528

Per Brett Slatkin 的“Effective Python: 59 Specific Ways to Write Better Python”,第 20 条:使用 None 和 Docstrings 指定动态默认参数(第 48 页)

在 Python 中实现所需结果的约定是提供默认值 None 并在文档字符串中记录实际行为。

此实现确保对函数的每次调用要么接收默认列表,要么接收传递给函数的列表。

首选方法:

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]

“错误方法”可能存在合法用例,程序员希望共享默认列表参数,但这更有可能是例外而不是规则。


M
Marcin

这里的解决方案是:

使用 None 作为您的默认值(或 nonce 对象),并打开它以在运行时创建您的值;或使用 lambda 作为默认参数,并在 try 块中调用它以获取默认值(这是 lambda 抽象的用途)。

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


这不能回答问题。
j
joedborg

您可以通过替换对象(因此与范围的关系)来解决这个问题:

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

丑陋,但它的工作原理。


如果您使用自动文档生成软件来记录函数预期的参数类型,这是一个很好的解决方案。如果 a 为 None 则将 a=None 设置为 [] 并不能帮助读者一目了然地理解预期的内容。
很酷的想法:重新绑定该名称保证它永远不会被修改。我真的很喜欢。
这正是做到这一点的方法。 Python 不会制作参数的副本,因此您可以明确地制作副本。获得副本后,您可以随意修改,不会产生任何意外的副作用。
不过,这并不能回答问题。
S
Saish

当我们这样做时:

def foo(a=[]):
    ...

...如果调用者没有传递 a 的值,我们将参数 a 分配给 未命名 列表。

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

def foo(a=pavlo):
   ...

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

如果 pavlo 是可变的(可修改的),并且 foo 最终修改它,我们会注意到下一次调用 foo 时未指定 a 的效果。

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

 >>> foo()
 [5]

现在,pavlo 是 [5]。

再次调用 foo() 会再次修改 pavlo

>>> foo()
[5, 5]

调用 foo() 时指定 a 可确保不触及 pavlo

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

所以,pavlo 仍然是 [5, 5]

>>> foo()
[5, 5, 5]

b
bgreen-litl

我有时会利用这种行为来替代以下模式:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

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

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

我已经使用它来实例化访问外部资源的客户端类,以及创建用于记忆的字典或列表。

由于我认为这种模式并不广为人知,因此我确实在其中发表了简短的评论,以防止将来出现误解。


我更喜欢为 memoization 添加一个装饰器,并将 memoization 缓存放在函数对象本身上。
此示例不会替换您显示的更复杂的模式,因为您在默认参数示例中的 def 时间调用 _make_singleton,但在全局示例中的调用时间。真正的替换将使用某种可变框作为默认参数值,但添加参数提供了传递替代值的机会。
P
Przemek D

每个其他答案都解释了为什么这实际上是一种不错的理想行为,或者为什么您无论如何都不应该需要它。我是为那些想要行使权利以使语言服从自己的意愿的顽固的人,而不是相反的方式。

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

import inspect
from copy import deepcopy  # copy would fail on deep arguments like nested dicts

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(deepcopy(arg))
        return function(*new_args, **kw)
    return wrapper

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

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

foo() # '[5]'
foo() # '[5]' -- as desired

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

# 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

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

请务必注意,如果您尝试使用关键字 args,上述解决方案将中断,如下所示:

foo(a=[4])

可以调整装饰器以允许这样做,但我们将此作为练习留给读者;)


如果默认参数很深,这也会中断,例如 {"grandparent": {"parent": {"child": "value"}}} 。只有顶级字典按值复制,其他字典按引用复制。出现此问题是因为您使用 copy 而不是 deepcopy
@Flimm我发现您的短语“这中断了”相当不公平,因为它似乎表明整个概念存在某种缺陷,而实际上它只是实现的一个小细节。但是仍然感谢您的评论,我将编辑和改进我的答案。
N
Norfeldt

这个“bug”给了我很多加班时间!但我开始看到它的潜在用途(但我希望它仍然在执行时)

我会给你一个我认为有用的例子。

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()

打印以下内容

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

你的例子似乎不太现实。为什么要传递 errors 作为参数而不是每次都从头开始?
M
Mark Ransom

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

我看到有 3 种情况您可能会遇到此问题:

您打算将参数修改为函数的副作用。在这种情况下,使用默认参数是没有意义的。唯一的例外是当您滥用参数列表来获得函数属性时,例如cache={},并且您根本不会期望使用实际参数调用该函数。您打算不修改参数,但您不小心修改了它。这是个bug,修复一下。您打算修改参数以在函数内部使用,但没想到修改可以在函数外部查看。在这种情况下,您需要制作参数的副本,无论它是否是默认值! Python 不是按值调用的语言,因此它不会为您制作副本,您需要明确说明它。

问题中的示例可能属于第 1 类或第 3 类。奇怪的是,它既修改了传递的列表又返回了它;你应该选择其中一个。


“做错事”就是诊断。也就是说,我认为有时 =None 模式很有用,但通常你不想修改如果在这种情况下传递了一个可变的 (2)。 cache={} 模式实际上是一个仅面试的解决方案,在实际代码中您可能想要 @lru_cache
完全不同意,在许多情况下,这绝对是一个设计缺陷,而不是程序员做某事
我从来没有遇到过 OP 的问题,即使它被高度支持,因为默认参数是可变的对我来说是奇怪的设计。
@MarkRansom如果我们认为副作用是可以的,那么将默认参数修改为副作用函数的一部分并没有错。假设您有一个对列表执行某些操作并返回列表的函数。我们要确保函数总是返回一个列表。然后将空(或非空)列表作为默认值非常有意义。该语言违反了大部分新 Python 程序员的期望。为什么他们错了,而语言是对的?如果语言有相反的行为,你会提出相反的论点吗?
@MarkRansom 不,他们不是;例如,JavaScript doesn't have this design flaw
P
Prune

只需将功能更改为:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

不过,这并不能回答问题。
M
MisterMiyagi

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

定义函数会影响两个范围:定义范围包含函数,以及执行范围包含函数。虽然很清楚块如何映射到范围,但问题是 def <name>(<args=defaults>): 属于哪里:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

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

由于 parameter 是一个常量名称,我们可以与 def name 同时“评估”它。这也有一个优点,它生成具有已知签名的函数 name(parameter=...):,而不是裸的 name(...):

现在,何时评估 default

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

这两个选择也不等价:如果在定义时评估 default,它仍然会影响执行时间。如果在执行时评估 default,它不能影响定义时间。选择“at definition”可以表达两种情况,而选择“at execution”只能表达一种情况:

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
    ...

“一致性已经说“在定义时”:def <name>(<args=defaults>): 的其他所有内容也最好在定义时进行评估。我不认为结论是从前提得出的。仅仅因为两件事在同一行并不意味着它们应该在同一范围内进行评估。 default 与该行的其余部分不同:它是一个表达式。评估表达式与定义函数是一个非常不同的过程。
@LarsH 函数定义是在 Python 中评估的。无论是来自语句 (def) 还是表达式 (lambda),都不会改变创建函数意味着评估——尤其是其签名。默认值是函数签名的一部分。这并不意味着默认值 必须 被立即评估——例如,类型提示可能不会。但这当然表明他们应该这样做,除非有充分的理由不这样做。
好的,创建一个函数在某种意义上意味着评估,但显然不是在定义时评估其中的每个表达式。大多数不是。我不清楚在什么意义上签名在定义时被特别“评估”,而不是函数体被“评估”(解析为合适的表示形式);而函数体中的表达式显然没有完全评估。从这个角度来看,一致性会说签名中的表达式也不应该被“完全”评估。
我并不是说你错了,只是你的结论不仅仅来自一致性。
@LarsH 默认值既不是正文的一部分,我也不是声称一致性是唯一的标准。你能提出如何澄清答案的建议吗?
u
user2384994

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

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

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

[]是一个对象,所以python将[]的引用传递给a,即a只是一个指向[]的指针,它作为一个对象存在于内存中。 [] 只有一份副本,但有很多引用。对于第一个 foo(),列表 [] 通过 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]

[] 是对象,None 也是对象(前者是可变的,后者是不可变的。但可变性与问题无关)。 None 在空间的某个地方,但我们知道它在那里,并且那里只有一个 None 的副本。因此,每次调用 foo 时,都会将 items 评估(与仅评估一次的某些答案相反)为 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]

foo(1) 的调用使项目指向一个列表对象 [],其地址为 11111111。列表的内容在后续的 foo 函数中更改为 1,但地址没有更改,仍然是 11111111。然后 foo(2,[]) 来了。虽然 foo(2,[]) 中的 [] 与调用 foo(1) 时的默认参数 [] 内容相同,但它们的地址不同!由于我们显式提供了参数,items 必须获取这个新 [] 的地址,例如 2222222,并在进行一些更改后返回它。现在 foo(3) 被执行。由于只提供了 x,因此 items 必须再次采用其默认值。默认值是多少?在定义 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)

每个按钮都可以包含一个不同的回调函数,该函数将显示不同的 i 值。我可以提供一个例子来说明这一点:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

如果我们执行 x[7](),我们将按预期得到 7,而 x[9]() 将给出 9,即 i 的另一个值。


你的最后一点是错误的。试一试,您会发现 x[7]()9
“python 按值传递基本数据类型,即,将值的本地副本复制到局部变量”是完全不正确的。我很惊讶有人明明很了解 Python,却对基本原理有如此可怕的误解。 :-(