Warning: file_get_contents(/data/phpspider/zhask/data//catemap/0/xml/12.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Python “最小惊奇”和可变默认参数_Python_Language Design_Default Parameters_Least Astonishment - Fatal编程技术网

Python “最小惊奇”和可变默认参数

Python “最小惊奇”和可变默认参数,python,language-design,default-parameters,least-astonishment,Python,Language Design,Default Parameters,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] >>

任何修修补补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

编辑:

巴泽克举了一个有趣的例子。结合您的大多数评论,特别是尤塔尔的评论,我进一步阐述了:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]
对我来说,设计决策似乎与参数的范围有关:在函数内部还是与函数一起

在函数内部进行绑定意味着在调用函数时,x实际上被绑定到指定的默认值,而不是定义,这将带来一个严重的缺陷:定义行将是混合的,因为函数对象的部分绑定将在定义时发生,以及函数调用时默认参数的部分赋值


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

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

def eat(food=fruits):
    ...
当我看到eat的声明时,最不令人惊讶的是,如果没有给出第一个参数,它将等于tuple apples、bananas、loganberries

然而,假设在代码的后面,我会这样做

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")
然后,如果默认参数是在函数执行时绑定的,而不是在函数声明时绑定的,那么我将以一种非常糟糕的方式吃惊地发现结果已经被更改了。这比发现上面的foo函数正在改变列表更令人震惊

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

StringBuffers=new StringBufferHello World!; 映射计数=新HashMap; 数一数二,五;; s、 追加!!!!; System.out.println counts.get;//这行吗? 现在,我的映射在放入映射时是使用StringBuffer键的值,还是通过引用存储键?不管怎样,总有人感到惊讶;或者是试图使用与放入对象的值相同的值从地图中取出对象的人,或者,即使他们使用的键与用于将其放入映射的对象完全相同,但似乎无法检索对象的人,这实际上就是Python不允许其可变内置数据类型用作字典键的原因

您的示例是一个很好的例子,Python新手会感到惊讶和痛恨。但我认为,如果我们解决了这个问题,那只会造成一种不同的情况,他们会被咬,而这种情况会更加不直观。此外,在处理可变变量时总是这样;您总是会遇到这样的情况:根据编写的代码,某些人可能会直觉地期望出现一种或相反的行为


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

原因很简单,代码执行时会进行绑定,函数定义也会执行,嗯。。。定义函数时

比较一下:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)
这段代码遭受完全相同的意外事件。香蕉是一个类属性,因此,当您向其添加内容时,它将添加到该类的所有实例中。原因完全相同

这只是它的工作方式,在函数情况下使它以不同的方式工作可能会很复杂,在类情况下可能不可能,或者至少会大大降低对象实例化的速度,因为您必须保留类代码,并在创建对象时执行它

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

也就是说,它应该在任何优秀的Python教程中占据突出位置。因为正如你提到的,每个人都会遇到这个问题
更高版本。

这种行为很容易解释为:

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

a不变-每次赋值调用都会创建新的int对象-打印新对象 b不变-新数组是根据默认值生成并打印的 c更改-对同一对象执行操作-并打印
你要问的是为什么会这样:

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()
_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
除了显式调用funcNone的情况,None,我们将忽略它

换句话说,与其计算默认参数,为什么不存储每个参数,并在调用函数时计算它们呢


一个答案可能是正确的,它将有效地将每个带有默认参数的函数转换为闭包。即使数据都隐藏在解释器中,而不是完全关闭,数据也必须存储在某个地方。这样会更慢,占用更多内存。

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

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

print_tuple()        #1
print_tuple((1,2,3)) #2
我给你一个提示。以下是拆卸步骤,请参见:

1. 2. 我怀疑经验丰富的行为是否有实际用途,谁真的在C中使用了静态变量,而没有滋生bug


如您所见,使用不可变的默认参数有一个性能优势。如果它是一个经常调用的函数,或者默认参数需要很长时间来构造,那么这可能会有所不同。另外,请记住Python不是C语言。在C语言中,常量是非常自由的。在Python中,你没有这种优势。

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

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

a = []
您希望得到一个由a引用的新列表

为什么a=[]应该在

在函数定义而不是调用时实例化新列表? 这就像你在问用户是否没有提供参数,然后实例化一个新列表,并像调用方生成一样使用它。 我认为这是不明确的:

def x(a=datetime.datetime.now()):
def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []
def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
用户,是否希望a默认为定义或执行x时对应的日期时间? 在本例中,与前一例一样,我将保持相同的行为,就好像默认参数赋值是函数datetime.now在函数调用时调用的第一条指令一样。 另一方面,如果用户想要定义时间映射,他可以写:

b = datetime.datetime.now()
def x(a=b):
我知道,我知道:这是个了结。或者,Python可能会提供一个关键字来强制定义时间绑定:

def x(static a=b):

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

一,。演出

如果使用了调用时求值,那么每次在没有参数的情况下使用函数时都会调用昂贵的函数。每次调用都要付出昂贵的代价,或者需要在外部手动缓存该值,从而污染名称空间并增加冗余

二,。强制界参数

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

funcs = [ lambda i=i: i for i in range(10)]
这将返回返回0,1,2,3的函数列表。。。分别地如果行为发生改变,它们会将i绑定到调用时间值i,因此您将得到一个全部返回9的函数列表

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

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]
三,。内省

以代码为例:

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

但是,我们已经失去了内省的能力,无法查看默认参数是什么。因为这些对象还没有被构造,我们无法在没有实际调用函数的情况下获得它们。我们所能做的最好的事情就是存储源代码并将其作为字符串返回。

最短的答案可能是定义为执行,因此整个参数没有严格意义。作为一个更人为的例子,您可以引用以下内容:

def a(): return []

def b(x=a()):
    print x
希望这足以说明在def语句执行时不执行默认参数表达式并不容易或没有意义,或者两者兼而有之

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

可能是真的:

有人正在使用每种语言/库功能 在这里改变行为是不明智的,但是 坚持以上两个特点,并提出另一点是完全一致的:

这是一个令人困惑的特性,在Python中很不幸。 其他答案,或者至少其中一些答案要么给出第1点和第2点,而不是第3点,要么给出第3点,淡化第1点和第2点。但这三点都是正确的

在中途换马可能会造成严重的破坏,而将Python更改为直观地处理Stefano的开头片段可能会产生更多的问题。也许有一个非常了解Python内部结构的人可以解释一个雷区的后果。但是,


现有的行为不是Pythonic的,Python是成功的,因为很少有关于该语言的内容违反了最不令人惊讶的原则。这是一个真正的问题,不管根除它是否明智。这是一个设计缺陷。如果你通过追踪行为更好地理解语言,我可以说C++做了这一切,而且更多。通过导航,例如,细微的指针错误,您可以学到很多东西。但这并不是Python式的:那些对Python足够关心并坚持面对这种行为的人是被这种语言所吸引的人,因为Python比其他语言的惊喜要少得多。当涉猎者和好奇者惊讶于不是因为一个设计fl,而是因为一个隐藏的逻辑谜题,而不是因为Python的工作而被吸引的程序员的直觉时,他们就变成了Python爱好者。

事实上,这不是一个设计缺陷,也不是因为内部原因,或者表演。 这仅仅是因为Python中的函数是一级对象,而不仅仅是一段代码

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

无论如何,Effbot对中这种行为的原因有很好的解释。
我发现它非常清楚,我真的建议阅读它,以便更好地了解函数对象是如何工作的。

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

>>> 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带有一个默认参数,不应该修改一个参数,不管它是显式传入的还是获得了默认值。您的代码应该不使用可变参数,除非从上下文/名称/文档中可以清楚地看到应该修改这些参数。使用作为参数传入的可变值作为本地临时变量是一个非常糟糕的主意,不管我们是否使用Python,也不管是否涉及默认参数


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

这里的解决方案是:

使用None作为默认值或nonce对象,并在运行时打开它来创建值;或 使用lambda作为默认参数,并在try块中调用它以获取默认值这是lambda抽象的目的。
第二个选项很好,因为函数的用户可以传入一个可调用的,它可能已经存在,例如类型。如果您考虑到以下因素,这种行为并不奇怪:

赋值尝试时只读类属性的行为,以及 功能是在公认的答案中解释得很好的对象。 在本线程中,2的角色已被广泛地介绍。1很可能是引起惊讶的因素,因为当来自其他语言时,这种行为是不直观的

1在Python中进行了描述。尝试将值分配给只读类属性时:

…在最内部范围之外找到的所有变量都是 只读试图写入这样一个变量只会创建一个 最内层范围中的新局部变量,使 命名外部 变量保持不变

回顾最初的例子并考虑以上几点:

def foo(a=[]):
    a.append(5)
    return a
这里foo是一个对象,a是foo的一个属性,可在foo.func_defs[0]中找到。因为a是一个列表,所以a是可变的,因此是foo的读写属性。当函数被实例化时,它被初始化为签名指定的空列表,并且只要函数对象存在,它就可以被读取和写入

在不重写默认值的情况下调用foo将使用foo.func_defs中的默认值。在本例中,foo.func_defs[0]用于函数对象的代码范围内的函数。对change foo.func_defs[0]的更改,它是foo对象的一部分,在执行foo中的代码之间保持不变

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

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]在局部范围中作为L不可用。 在L=[]时,由于foo.func_defs[0]的属性为只读,因此赋值无法成功。 根据1,在局部范围中创建一个新的局部变量(也称为L),并用于函数调用的其余部分。因此,foo.func_defs[0]在将来调用foo时保持不变。 报告的相关部分:

执行函数定义时,从左到右计算默认参数值。这意味着在定义函数时,表达式只计算一次,并且每次调用都使用相同的“预计算”值。当默认参数是可变对象(如列表或字典)时,了解这一点尤为重要:如果函数修改对象(例如,通过向列表中添加项),则默认值实际上已修改。这通常不是我们的初衷。解决此问题的一种方法是使用None作为默认值,并在函数体中显式测试它,例如:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

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]
解决方案:复印件 一个绝对安全的解决方案是先复制或深度复制输入对象,然后对该副本执行任何操作

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[:]或listsome\u list那样轻松复制。每个对象也可以通过copy.copyany_对象进行复制,或者通过copy.deepcopy进行更彻底的复制。如果可变对象是由可变对象组成的,则后者很有用。有些对象基本上是基于副作用的,如文件对象,不能通过复制进行有意义的复制

示例问题

它不应该保存在该函数返回的实例的任何公共属性中。假设实例的私有属性不应该根据约定从该类或子类之外进行修改。i、 e._var1是一个私有属性

结论: 输入参数对象不应就地修改,也不应绑定到函数返回的对象中。如果我们更喜欢没有副作用的编程,这是强烈推荐的。见前两段与此相关。

二, 只有当实际参数上的副作用是必需的,而默认参数上的副作用是不需要的,那么有用的解决方案是def…var1=None:如果var1是None:var1=[]


3在某些情况下是。

您可以通过替换对象来解决此问题,因此可以使用范围替换领带:

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

难看,但它可以工作。

一个简单的解决方案,使用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]

这个错误给了我很多加班时间!但我开始看到它的潜在用途,但我还是希望它能在执行时使用

我会给你一个有用的例子

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

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


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

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

[]是一个对象,因此python将[]的引用传递给a,即a只是指向[]的指针,它作为对象存在于内存中。但是,[]只有一个副本,有许多引用。对于第一个foo,list[]更改为by append方法。但是请注意,列表对象只有一个副本,而这个对象现在变为。当运行第二个foo时,effbot网页所说的不再评估项目是错误的。a被计算为列表对象,但现在该对象的内容为。这就是通过引用传递的效果!foo3的结果也可以用同样的方法很容易地推导出来

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

第二名========

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]
[]是一个对象,所以不是。前者是可变的,而后者是不可变的。但是易变性与这个问题无关。没有一个在空间的某个地方,但我们知道它在那里,那里只有一个副本。因此,每次调用foo时,都会对项进行求值,而不是对某个答案进行求值,即只对其求值一次,结果为None,更清楚地说,是None的引用或地址。然后在foo中,item被更改为[],即指向另一个具有不同地址的对象

第三名=======

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

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]
foo1的调用使items指向一个地址为11111111的列表对象[]。在sequel的foo函数中,列表的内容更改为,但地址没有更改,仍然是11111111。然后,foo2,[]就来了。虽然在调用foo1时,foo2中的[]与默认参数[]的内容相同,但它们的地址不同!因为我们显式地提供了参数,所以items必须获取这个新[]的地址,比如2222222,并在进行一些更改后返回它。现在执行foo3。由于只提供了x,因此项必须再次采用其默认值。默认值是多少?它是在定义foo函数时设置的:位于11111中的list对象。因此,这些项被评估为具有元素1的地址11111111。位于2222222的列表还包含一个元素2,但它不再由items指向。因此,追加3将生成项[1,3]

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

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.

def foo(a=[]):
    ...
。。。如果调用者没有传递a的值,我们将参数a分配给一个未命名的列表

为了简化本次讨论,让我们暂时为未命名列表命名。帕夫洛怎么样

在任何时候,如果打电话的人没有告诉我们a是什么,我们就会重复使用pavlo

如果pavlo是可变可修改的,而foo最终对其进行了修改,那么我们在下次调用foo时会注意到一个效果,而不指定a

这就是你们看到的,记住,pavlo被初始化为[]:

 >>> foo()
 [5]
现在,帕夫洛是[5]

再次调用foo会再次修改pavlo:

在调用foo时指定一个可确保不触及pavlo

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]
因此,帕夫洛仍然是[5,5]


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

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()
我用它来实例化访问外部资源的客户机类,也用来创建用于记忆的dict或列表


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

这个话题已经很忙了,但从我在这里读到的内容来看,以下内容帮助我认识到它的内部工作原理:

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
Python防御5分 简单性:行为在以下意义上是简单的: 大多数人只会落入这个陷阱一次,而不是几次

一致性:Python总是传递对象,而不是名称。 显然,默认参数是函数的一部分 标题不是功能体。因此,应当对其进行评估 在模块加载时且仅在模块加载时,除非嵌套,否则 在函数调用时

有用性:正如弗雷德里克·伦德在他的解释中指出的那样 当然 当前行为对于高级编程非常有用。 节约使用

足够的文档:在最基本的Python文档中, 在教程中,这个问题被大声宣布为 第节第一小节中的重要警告 . 警告甚至使用了黑体字, 很少在标题之外使用。 RTFM:阅读精细手册

元学习:陷入困境 陷阱实际上是一个非常复杂的问题 至少如果你是一个反思型学习者, 因为你以后会更好的理解这一点 以上的一致性将 教你很多关于Python的知识


只需将函数更改为:

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

我将演示另一种结构,将默认列表值传递给一个函数,它与字典同样有效

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

可能是错误的方法…:

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

根据Brett Slatkin的有效Python:59编写更好Python的具体方法,第20项:使用None和DocString指定动态默认参数p。四十八

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

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

首选方法:

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

为什么不反省一下? 我真的很惊讶没有人在callables上执行Python2和3 apply提供的深刻的内省

给出一个简单的小函数func,定义如下:

>>> def func(a = []):
...    a.append(5)
当Python遇到它时,它要做的第一件事就是编译它,以便为这个函数创建一个代码对象。完成此编译步骤时,Python计算*并将默认参数存储在函数对象本身的空列表[]中。正如上面提到的答案:列表a现在可以被视为函数func的成员

所以,让我们做一些内省,一个前后检查列表是如何在函数对象内部展开的。为此,我使用Python3.x,对于Python2,同样适用于Python2中的use\uuuu defaults\uuuuu或func\u defaults;是的,同一件事有两个名字

执行前的功能: Python执行此定义后,将采用此处指定的任何默认参数a=[]和相关部分:Callables:

>>> func.__defaults__
([],)
好的,一个空列表作为_默认值_中的单个条目,正如预期的那样

执行后的功能: 现在让我们执行此函数:

>>> func()
现在,让我们再看看那些默认值:

惊讶?对象内部的值会发生变化!对函数的连续调用现在只需附加到嵌入的列表对象:

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

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

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []
由于每次都会重新执行函数体,因此如果没有为a传递参数,则始终会得到一个新的空列表

要进一步验证uuu defaults_uuuu中的列表是否与函数func中使用的列表相同,只需更改函数以返回函数体中使用的列表a的id即可。然后,将其与\uuuu defaults\uuuu中的\uuuu defaults\uuuu位置[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
您会注意到,在构建函数并将其绑定到名称栏的过程之前,会调用输入。

Python:可变的默认参数 在将函数编译为函数对象时,会计算默认参数。当被函数多次使用时,它们是并保持相同的对象

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

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

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

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""
几乎完全等同于此:

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()
_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是t 每次引用该列表时,他都会这样做, 观察到当第二次调用使用该列表的函数时,该列表保持不变, 按照我方便地为您编号的来源打印输出的顺序: 示例.py

并使用python example.py运行它:

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

正如政府所说:

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

def x(a=datetime.datetime.now()):
def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []
def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
这不是设计缺陷。任何被这件事绊倒的人都是在做错事

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

您打算修改参数作为函数的副作用。在这种情况下,使用默认参数是没有意义的。唯一的例外是当您滥用参数列表来拥有函数属性时,例如cache={},并且您根本不需要使用实际参数调用函数。 您打算不修改参数,但意外地修改了它。这是一个错误,修复它。 您打算修改参数以便在函数内部使用,但不希望修改在函数外部可见。在这种情况下,您需要复制参数,无论它是否为默认参数!Python不是一种按值调用的语言,因此它不会为您创建副本,您需要明确说明它。
问题中的例子可以归入第1类或第3类。奇怪的是,它修改并返回传递的列表;您应该选择一个或另一个。

TLDR:Define time defaults是一致的,严格来说更具表现力

定义函数会影响两个作用域:包含函数的定义作用域和函数包含的执行作用域。虽然块如何映射到作用域非常清楚,但问题是def:属于哪里:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope
def名称部分必须在定义范围内求值-毕竟,我们希望名称在定义范围内可用。仅在函数内部对其进行求值将使其无法访问

因为参数是一个常量名,所以我们可以在计算def name的同时计算它。这还有一个优点,即它生成的函数具有一个名为nameparmeter=…:,而不是一个空名称…:,的已知签名

现在,何时评估默认值


一致性已经在定义中说明了:def:的所有其他内容也在定义中得到了最好的评估。推迟部分时间将是一个惊人的选择

这两个选项也不是等价的:如果在定义时计算默认值,它仍然会影响执行时间。如果在执行时计算默认值,则它不会影响定义时间。选择“在定义时”可以表示两种情况,而选择“在执行时”只能表示一种情况:

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净化功能: def包装*a,**千瓦: 存储默认值 默认值=python2的inspect.getargspecfunction.defaults 构造一个新的参数列表 新参数=[] 对于i,枚举默认值中的arg: 允许传递位置参数 如果我在rangelena: 新附录[i] 其他: 复制值 新参数appendcopyarg 返回函数*新参数,**千瓦 返回包装器 现在,让我们使用此装饰器重新定义函数:

@净化 def fooa=[]: a、 附件5 归还 foo'[5]' foo'[5]'-根据需要 对于具有多个参数的函数,这一点尤其简洁。比较:

“正确”的方法 def bara=无,b=无,c=无: 如果a为无: a=[] 如果b为无: b=[] 如果c为无: c=[] 最后做实际工作 与

讨厌的装修工黑客 @净化 def bara=[]、b=[]、c=[]: 哇,开箱即用! 需要注意的是,如果您尝试使用关键字args,则上述解决方案会中断,如下所示:

fooa=[4]
可以调整装饰器以允许这样做,但我们将此作为练习留给读者

我认为这是一个争论的问题。您正在作用于一个全局变量。在代码中任何地方执行的涉及全局变量的任何计算现在都将正确地引用蓝莓、芒果。默认参数可以像其他任何情况一样。事实上,我不认为我
请看你的第一个例子。首先,我不确定我是否喜欢这样修改初始值设定项,但如果我这样做了,我希望它的行为与您描述的完全一样-将默认值更改为蓝莓、芒果。默认参数与任何其他情况一样。出乎意料的是,参数是全局变量,而不是局部变量。这又是因为代码是在函数定义处执行的,而不是在调用时执行的。一旦你明白了这一点,同样的道理也适用于课堂,这是非常清楚的。我发现这个例子是误导性的,而不是精彩的。如果某个随机函数附加到水果上而不是分配给它,eat的行为就会改变。现在的精彩设计到此为止。如果使用其他地方引用的默认参数,然后从函数外部修改引用,那么就是自找麻烦。真正的WTF是当人们定义一个新的默认参数、一个列表文字或一个对构造函数的调用时,你只是显式地声明了全局并重新分配了元组-如果eat在这之后的工作方式不同,这绝对没有什么奇怪的。你如何定义一个类的每个实例都不同的类属性?如果每个实例都不同,它就不是一个类属性。类属性是类上的属性。因此得名。因此,它们对于所有实例都是相同的。如何在类中定义一个属性,而该属性对于类的每个实例都是不同的?为那些无法确定不熟悉Python命名规则的人可能会询问类的普通成员变量的人重新定义。@Kievieli:您指的是类的普通成员变量。:-您可以通过在任何方法中说self.attribute=value来定义实例属性。例如_init__.@Kieveli:两个答案:你不能,因为你在类级别定义的任何东西都将是一个类属性,任何访问该属性的实例都将访问同一个类属性;你可以,/sort/,通过使用propertys-这实际上是类级函数,作用类似于普通属性,但通过使用self.attribute=value将属性保存在实例中,而不是像Lennart说的那样保存在类中。实际上,add是一个糟糕的例子,但整数是不可变的仍然是我的主要观点。在检查b是否设置为[]后,我意识到这一点,使我懊恼不已。当b设置为[]时,uuu add_uuu[1]返回[1],但即使列表是可变的,也会使b仍然[]。我的错。@ANon:有uu iadd uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu-您可以这样做:def xa=None:然后,如果a为None,则设置a=datetime.datetime.now谢谢。我真的弄不明白为什么这件事让我烦透了。你用最少的模糊和混乱完成了它。当有人从C++编程系统中开始,有时会天真地翻译语言特征时,这个假朋友把我踢进了脑袋里的大时间,就像类属性一样。我明白为什么事情会这样,但我还是忍不住不喜欢它,不管它会带来什么积极的结果。至少这是与我的经验相反的,我可能希望永远不会忘记它……安德烈亚斯:一旦你用Python已经足够长,你就开始看到Python是如何将事物解释为类属性的方式——这只是因为C++和java语言和C.的特殊的怪癖和局限性。类{}块的内容被解释为属于实例是有意义的:但是,当类是一级对象时,很明显,它们在内存中的内容在代码中反映它们的内容是很自然的事情。在我的书中,规范的结构并不是怪癖或限制。我知道这可能是笨拙和丑陋的,但你可以称之为某种东西的定义。在我看来,动态语言有点像无政府主义者:当然,每个人都是自由的,但你需要有人清空垃圾,铺路。我想我老了…:函数定义在模块加载时执行。函数体在函数调用时执行。默认参数是函数定义的一部分,而不是函数体的一部分。对于嵌套函数来说,它变得更加复杂。它不需要是闭包-一种更好的方法是简单地将字节码创建默认值作为第一行代码-在编译主体之后,无论如何-参数中的代码和主体中的代码之间没有真正的区别。是的,但是它仍然会减慢Python的速度,这实际上是非常令人惊讶的,除非您对类定义执行相同的操作,这将使它变得非常缓慢,因为每次实例化一个类时都必须重新运行整个类定义。如前所述,修复比问题更令人惊讶。同意Lennart的观点。正如Guido喜欢说的,对于每一种语言特性或标准库,都有人在使用它。现在改变它将是疯狂的,我们只是在探索为什么我
事情就是这样。如果它一开始就做了延迟默认评估,那就不一定令人惊讶了。毫无疑问,这样一个核心——语法分析的差异——会对整个语言产生广泛的、可能是许多模糊的影响。如果每种语言都有一个函数来创建默认参数,而不是一个值,那么你也可以实现内省。inspect模块将只调用该函数。@SilentGhost:我指的是行为是否被更改以重新创建它-创建一次是当前行为,以及为什么存在可变默认问题。@yairchu:这假设构造是安全的,因此ie没有副作用。反省args不应该做任何事情,但是评估任意代码可能最终会产生效果。不同的语言设计通常意味着编写不同的东西。您的第一个示例可以很容易地写为:_昂贵=昂贵;def fooarg=_昂贵,如果您特别不想重新计算它。@Glenn-这就是我所指的外部缓存变量-它有点冗长,但您的命名空间中会有额外的变量。对于阅读上述答案的任何人,我强烈建议您花点时间阅读链接的Effbot文章。除了所有其他有用的信息外,关于如何将此语言功能用于结果缓存/记忆的部分非常便于了解!即使它是一个一级对象,也可以设想一种设计,其中每个默认值的代码都与对象一起存储,并在每次调用函数时重新计算。我并不是说这会更好,只是函数作为一级对象并不能完全排除它。对不起,任何被认为是Python中最大的WTF的东西都肯定是一个设计缺陷。这是每个人在某一点上的一个bug来源,因为最初没有人期望这种行为——这意味着它不应该一开始就这样设计。我不在乎他们必须跳出什么圈套,他们应该设计Python,使默认参数是非静态的。无论这是否是设计缺陷,您的回答似乎暗示这种行为在某种程度上是必要的、自然的和明显的,因为函数是一流的对象,而事实并非如此。Python有闭包。如果在函数的第一行用赋值替换默认参数,则每次调用都可能使用封闭范围中声明的名称对表达式求值。没有理由认为每次以完全相同的方式调用函数时都计算默认参数是不可能或不合理的。设计不是直接从函数是对象开始的。在您的范例中,建议将函数的默认值作为属性而不是属性来实现。补充问题-我毫不怀疑可变参数违反了普通人的最小惊讶原则,我看到初学者走到了这一步,然后英勇地用邮件元组替换邮件列表。尽管如此,可变参数仍然符合PythonZen Pep 20的规定,并且属于明显的为核心Python程序员所理解/利用的条款。推荐的使用文档字符串的解决方法是最好的,但是现在抵制文档字符串和任何书面文档的情况并不少见。就我个人而言,我更喜欢一个装饰者说@fixed_defaults。当我遇到这个问题时,我的论点是:为什么需要创建一个函数来返回一个可以选择性地传递给函数的可变变量?它要么改变一个可变表,要么创建一个新的可变表。为什么您需要用一个函数同时完成这两个任务?为什么要重写解释器以允许您在不向代码中添加三行代码的情况下执行此操作?因为我们在这里讨论重写解释器处理函数定义和调用的方式。对于一个几乎不需要的用例来说,这需要做很多工作。Python新手希望这个函数总是返回一个只有一个元素的列表:[5]。我是一个Python新手,我不希望出现这种情况,因为显然foo[1]将返回[1,5],而不是[5]。您的意思是,新手会期望在没有参数的情况下调用的函数总是返回[5]。这个问题问,为什么这[错误的方式]会得到如此实现?它没有问什么是正确的方法?,[为什么使用arg=None修复Python的可变默认参数问题?]*。新用户几乎总是对前者不太感兴趣,而对后者更感兴趣,因此,这有时是一个非常有用的链接/重复引用。-1尽管这是一个合理的观点,但这不是一个答案,我不同意。太多的特殊例外会产生他们自己的拐角案例。因此,在Python中,默认的[] ]在每次调用函数时保持[]都是非常有意义的,而且不知道把它作为缺省成语设置一个不幸的成语。

nt设置为None,然后在函数设置的主体中,如果参数==None:参数=[]?当人们想要一个天真的新来的人所期望的,如果你指定fTe==[],参数会自动默认为[]的值,这是不知道的吗?但是在Python中,语言的一部分精神是你不需要太多的深潜水;不管您对排序、大O和常量了解多少,array.sort都可以工作。作为无数示例之一,Python在数组排序机制中的妙处在于不需要深入研究内部。换言之,Python的美妙之处在于,人们通常不需要深入研究实现来获得能够正常工作的东西。还有一个解决方法…如果参数==None:argument=[],则失败。作为独立语句,语句x=[]意味着创建一个空列表对象,并将名称“x”绑定到它。因此,在def fx=[]中,还创建了一个空列表。它并不总是绑定到x,因此它会绑定到默认代理。稍后调用f时,默认值被拖出并绑定到x。因为是空列表本身被存储了起来,所以无论是否有任何东西被卡在其中,同一个列表是唯一可以绑定到x的东西。这怎么可能呢?虽然相关,但我认为这是一种独特的行为,因为我们期望append能够改变一个地方。在每次调用时都没有重新实例化默认的可变表,这是意外的一点。。。至少对我来说是这样@AndyHayden如果函数被期望修改参数,为什么默认值有意义呢?@AndyHayden我在这里留下了我自己的答案,并扩展了这种观点。让我知道你的想法。为了完整起见,我可能会将您的cache={}示例添加到其中。@而且我的回答的要点是,如果您意外地改变了参数的默认值而感到惊讶,那么您还有另一个错误,即您的代码可能会在未使用默认值时意外地改变调用方的值。请注意,如果没有使用ARG并分配真正的缺省值,如果ARG是不存在的,则不能解决这个问题,因此我认为这是一个反模式。如果您通过避免改变参数值来修复另一个bug,不管它们是否有默认值,那么您将永远不会注意到或关心这种惊人的行为。@AndyHayden如果您将self.foo=foo.copy放在其中,那么如果foo的默认值为[],又有什么害处呢?它的副本保护您免受可变参数的困扰,而不是在您真正想要默认值[]时将默认值设置为“无”。如果foo为None,当然可以编写:self.foo=[];else:self.foo=foo.copy,但是为什么您可以用一行替换4行,而这是您需要的4行之一,在函数定义中,默认参数的实际值是否更清晰?在使用自动文档生成软件记录函数所需的参数类型的情况下,这是一个很好的解决方案。如果a为None,则将a设置为[],这并不能帮助读者一眼就理解所期望的内容。好主意:重新绑定该名称可以保证它永远不会被修改。我真的很喜欢。这正是做这件事的方法。Python不复制参数,所以由您显式地复制。一旦你有了一份拷贝,你就可以随意修改,而不会有任何意外的副作用。你的最后一点是错误的。尝试一下,您会发现x[7]是9.python按值传递基本数据类型,即,将值的本地副本复制到本地变量是完全不正确的。我很惊讶,有人显然非常了解Python,但对基本原理却有如此可怕的误解-这句话通常不是我们想要的,解决这一问题的方法有点像他们记录了一个设计缺陷。@bukzor:陷阱需要注意并记录下来,这就是为什么这个问题很好,并且得到了这么多的支持。与此同时,陷阱并不一定需要消除。有多少Python初学者向修改它的函数传递了一个列表,并震惊地看到原始变量中出现了更改?然而,当您了解如何使用可变对象类型时,它们是非常棒的。我想这只能归结为对这个特殊陷阱的看法。这个短语“这不是通常的意图”,意思不是程序员真正想要发生的事情,也不是Python应该做的事情。@holdenweb哇,我来晚了。考虑到上下文,bukzor是完全正确的:当他们决定语言应该执行函数的定义时,他们记录的行为/结果并不是他们想要的。因为这是他们设计选择的意外结果,所以这是一个设计缺陷。如果这不是一个设计缺陷,我们甚至不需要提供一种解决方法。我们可以把它带到聊天和讨论中

这是怎么回事,但是语义已经被彻底讨论过了,没有人能想出一个合理的机制来创建调用时的默认值。一个严重的问题是,调用范围通常与定义范围完全不同,如果在调用时评估默认值,则名称解析不确定。绕道而行意味着你可以通过以下方式达到你想要的目的,但这不是Python设计中的一个错误。我希望你知道Python不是一种函数式编程语言。是的,Python是一种具有一些函数特性的多模式语言。不要因为你有锤子就把每个问题都弄得像钉子。其中许多都是Python最佳实践。Python有一个有趣的特性,其他特性是闭包和currying,这里没有提到。我还想补充一点,在这个后期阶段,Python的赋值语义被明确设计为避免必要的数据复制,因此创建副本,特别是深度副本,将对运行时和内存使用产生不利影响。因此,它们应该只在必要时使用,但新来者往往很难理解这一点。@holdenweb我同意。临时拷贝是保护原始可变数据不受可能修改它们的无关函数影响的最常用方法,有时也是唯一可能的方法。幸运的是,不合理地修改数据的函数被认为是一个bug,因此并不常见。我同意这个答案。我不明白为什么当你真的有别的意思时,建议使用def f a=None构造。复制是可以的,因为不应该改变参数。如果a为None:a=[1,2,3],则无论如何都要复制列表。我更喜欢添加一个用于记忆的装饰器,并将记忆缓存放在函数对象本身上。此示例不会替换您显示的更复杂的模式,因为在默认参数示例中,您在def time调用_make_singleton,但在全球示例中,在通话时间。真正的替换会使用某种可变框作为默认参数值,但添加参数会提供传递替代值的机会。我花了一年时间才发现这种行为会在生产中弄乱我的代码,最终删除了一个完整的功能,直到我偶然碰到这个设计缺陷。我用的是Django。由于登台环境没有太多的请求,这个bug对QA没有任何影响。当我们上线并同时收到许多请求时,一些实用程序函数开始覆盖彼此的参数!制造安全漏洞、bug等等。@oriadam,恕我冒犯,但我想知道您以前是如何在没有遇到这些问题的情况下学习Python的。我现在正在学习Python,这个可能的陷阱就在第一次提到默认参数的时候。如本答复第4点所述。我想,阅读您用于创建生产软件的语言的官方文档,其寓意是相当不友好的。此外,如果在我正在进行的函数调用之外还调用了未知复杂性的函数,我会感到惊讶。@oriadam,您的公司需要在开发、登台和生产环境中使用他们编写的语言进行代码审查和实际的专家程序员。新手bug和坏代码习惯不应该出现在生产代码id中。。。需要最后一次验证,还是is操作员会回答相同的问题?@das-g is会做得很好,我只是使用idval,因为我认为它可能更直观。使用None作为默认值严重限制了“默认值”自省的有用性,所以我不认为这能很好地为默认值的工作方式辩护。延迟求值可以更好地保持函数默认值对双方都有用。实际上,对于新手来说,这可能有点混乱,因为a=a+[1]重载a。。。考虑将其改变为B= A+〔1〕;打印idb并添加一行a.append2。这将使两个列表上的+总是创建一个分配给b的新列表,而修改后的a仍然可以有相同的ida。做错事就是诊断。也就是说,我认为有时WARE=None模式是有用的,但通常情况下,如果在这种情况下传递了一个可变值,您不想修改它。cache={}模式实际上是一个只用于面试的解决方案,在您可能想要的实际代码中!完全不同意,在很多情况下这绝对是一个设计缺陷,而不是程序员在做什么wong@aCuria那么你有一个案例4和我介绍的案例3不同?我很想听听,请告诉我更多。Python的行为在这种情况下可能没有意义,但它在其他地方非常有用,改变它将是一场灾难。我从来没有遇到过OP的问题,尽管它的投票率如此之高,因为让默认参数可变对我来说是一种奇怪的设计。@markransem如果我们认为它的副作用是可以的,修改默认参数a没有什么错
这是副作用的一部分。假设您有一个函数,它对列表执行某些操作并返回列表。我们希望确保函数始终返回一个列表。那么,将空列表或非空列表作为默认值是非常有意义的。这种语言违背了新Python程序员的大部分期望。为什么他们错了,语言是对的?如果语言有相反的行为,你会提出相反的论点吗?一致性已经在定义中说明:def:的所有其他内容也在定义中得到最好的评估。我不认为结论来自于前提。仅仅因为两件事情在同一条线上并不意味着它们应该在同一范围内进行评估。默认值与行的其余部分不同:它是一个表达式。计算表达式与定义函数是完全不同的过程。@LarsH函数定义是用Python计算的。无论是来自语句def还是表达式lambda,创建函数都意味着求值,尤其是对其签名的求值。默认值是函数签名的一部分。这并不意味着必须立即评估默认值——例如,类型提示可能不会。但它肯定建议他们应该这样做,除非有充分的理由不这样做。好吧,创建函数在某种意义上意味着求值,但显然不是在定义时对其中的每个表达式求值。大多数都不是。我不清楚在什么意义上签名是在定义时被特别评估的,就像函数体被评估并解析为合适的表示一样;而函数体中的表达式显然不是完全意义上的。从这个角度来看,一致性会说签名中的表达式也不应该被完全评估。我并不是说你错了,只是你的结论并不仅仅来自一致性。@LarsH默认值不是正文的一部分,我也不是说一致性是唯一的标准。你能提出一个如何澄清答案的建议吗?你的例子似乎不太现实。为什么每次都要将错误作为参数传递,而不是从头开始?
>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)
def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []
>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True
def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2
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
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()
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']
def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []
def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope
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
    ...