“最少惊讶”和可变默认参数

长时间修改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中使用了静态变量,却没有滋生bug?)

编辑

巴切克举了一个有趣的例子。连同您的大多数评论,特别是Utaal的评论,我进一步阐述了:

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

在我看来,设计决策似乎与将参数范围放置在何处有关:在函数内部还是“一起”使用?

在函数内部进行绑定将意味着x在调用该函数(未定义)时将其有效地绑定到指定的默认值,这会带来严重的缺陷:def从绑定的一部分(该行的函数对象)将在定义时发生,部分(默认参数的分配)将在函数调用时发生。

实际行为更加一致:执行该行时将评估该行的所有内容,即在函数定义时进行评估。

Itachi2020/05/28 15:06:26

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在执行时评估,则不会影响定义时间。选择“在定义时”允许表达两种情况,而选择“在执行时”只能表达一种情况:

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
    ...
宝儿理查德2020/05/28 15:06:26

使用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]
王者一打九2020/05/28 15:06:24

实际上,这与默认值无关,除了在编写具有可变默认值的函数时,它经常会作为意外行为出现。

>>> 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,是否涉及默认参数。

如果您需要在计算内容的过程中破坏性地操作本地临时文件,并且需要从参数值开始进行操作,则需要进行复制。

Gil伽罗小宇宙2020/05/28 15:06:24

当我们这样做时:

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]

指定a呼叫时foo()确保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]
Tom凯2020/05/28 15:06:24

可能确实是:

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

坚持上述两个功能,并且仍然提出另一点是完全一致的:

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

其他答案,或者至少其中一些答案得分为1和2,但不是3,或者得分为3,淡化得分为1和2。但所有三个答案都是正确的。

的确,在此处中途更换马匹可能会造成重大损坏,并且通过更改Python以直观地处理Stefano的开头代码段可能会产生更多问题。确实可能是一个非常了解Python内部知识的人可以解释后果的雷区。然而,

现有的行为不是Python式的,Python是成功的,因为几乎没有什么语言违反了附近任何地方的最小惊讶原则这很糟糕。根除它是否明智是一个真正的问题。这是一个设计缺陷。如果您通过尝试找出行为来更好地理解该语言,那么可以说C ++可以完成所有这些工作,甚至更多。通过导航(例如)细微的指针错误,您学到了很多东西。但这不是Python风格的:关心Python足以在这种行为面前持之以恒的人是被该语言吸引的人,因为Python比其他语言少得多的惊喜。当涉猎者和好奇者成为一名Pythonista使用者时,他们惊讶地发现需要花很少的时间才能完成某项工作-不是因为设计漏洞-我的意思是隐藏的逻辑难题-消除了被Python吸引的程序员的直觉因为它可行

阿飞2020/05/28 15:06:24

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

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

丑陋,但是行得通。

小卤蛋2020/05/28 15:06:23

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

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

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

由于我认为这种模式并不为人所知,因此我做了简短的评论,以防止将来发生误解。

蛋蛋2020/05/28 15:06:23

这里的解决方案是:

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

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

西里神奇2020/05/28 15:06:23

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

def a(): return []

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

希望足以表明在def语句的执行时间不执行默认参数表达式不是一件容易的事,或者说没有道理,或者两者兼而有之。

我同意,当您尝试使用默认构造函数时,这是一个陷阱。

小卤蛋2020/05/28 15:06:22

我对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):
斯丁前端2020/05/28 15:06:22

您要问的是为什么这样:

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)的情况外,我们将忽略它。

换句话说,为什么不存储默认参数,而不是评估默认参数,并在调用函数时对其进行评估?

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

猴子村村2020/05/28 15:06:22

此行为很容易通过以下方式解释:

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

所以:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a 不变-每个分配调用都会创建一个新的int对象-打印新对象
  2. b 不变-从默认值构建新数组并打印
  3. c 更改-对同一对象执行操作-并打印
Chloe2020/05/28 15:06:22

好吧,原因很简单:绑定是在执行代码时完成的,而函数定义是在执行时定义的。

比较一下:

class BananaBunch:
    bananas = []

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

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

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

是的,这是意外的。但是一旦一分钱下降,它就完全适合Python的工作方式。实际上,这是一种很好的教学手段,一旦您了解了为什么会发生这种情况,您就会更好地使用python。

也就是说,它应该在任何优秀的Python教程中都非常突出。因为正如您提到的,每个人迟早都会遇到此问题。

GO2020/05/28 15:06:21

捍卫Python的5分

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

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

  3. 用途:正如Frederik Lundh在对“ Python中的默认参数值”的解释中所指出的那样,当前行为对于高级编程可能非常有用。(请谨慎使用。)

  4. 足够的文档:在最基本的Python文档中,该教程“更多关于定义函数”部分第一小节中 “重要警告”的形式大声宣布该问题警告甚至使用黑体字,很少在标题之外使用。RTFM:阅读精美的手册。

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

十刃2020/05/28 15:06:21

The relevant part of the documentation:

Default parameter values are evaluated from left to right when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call. This is especially important to understand when a default parameter is a mutable object, such as a list or a dictionary: if the function modifies the object (e.g. by appending an item to a list), the default value is in effect modified. This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function, e.g.:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin
鱼二水2020/05/28 15:06:21

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

一旦您想到这种方式,就完全有道理了:函数是根据其定义求值的对象;默认参数属于“成员数据”,因此它们的状态可能会从一个调用更改为另一个调用-就像在其他任何对象中一样。

无论如何,Effbot 在Python的Default Parameter Values中都很好地解释了这种现象的原因
我发现它很清晰,我真的建议您阅读它,以更好地了解函数对象的工作原理。