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

时间:2009-07-15 18:00:37

标签: 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]
>>> foo()

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

修改

Baczek做了一个有趣的例子。再加上你的大部分评论和尤其是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行将是“hybrid”意味着部分绑定(函数对象)将在定义时发生,并且部分(默认参数的赋值)在函数调用时发生。

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

33 个答案:

答案 0 :(得分:1490)

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

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

在任何情况下,Effbot都会在Default Parameter Values in Python中对此行为的原因进行非常好的解释。
我发现它非常清楚,我真的建议阅读它以更好地了解函数对象的工作原理。

答案 1 :(得分:257)

假设您有以下代码

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

def eat(food=fruits):
    ...

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

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

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

然后如果默认参数在函数执行而不是函数声明中被绑定,那么我会惊讶地发现水果已经被改变了(以非常糟糕的方式)。这比发现上面的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?

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

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

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

答案 2 :(得分:225)

AFAICS尚未发布documentation的相关部分:

  

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

答案 3 :(得分:107)

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

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

a = []

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

为什么<{1}}在

a=[]

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

def x(a=[]):

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

datetime.now()

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

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

答案 4 :(得分:78)

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

比较一下:

class BananaBunch:
    bananas = []

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

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

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

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

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

答案 5 :(得分:56)

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

<强> 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的调用时间值,这样您就可以获得所有返回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=[]

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

答案 6 :(得分:54)

为什么不反省?

真的感到惊讶没有人对Python(23申请)提供有关callables的富有洞察力的内省。

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

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

当Python遇到它时,首先要做的是编译它以便为这个函数创建一个code对象。在完成此编译步骤时, Python 评估 *然后存储默认参数(此处为空列表[])在函数对象本身中< / em>的。正如上面提到的答案所示:列表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__
([],)

O.k,这是一个空列表,作为__defaults__中的单个条目,正如预期的那样。

执行后的功能:

现在让我们执行此功能:

>>> func()

现在,让我们再次看到__defaults__

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

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

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

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

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

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

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

要进一步验证__defaults__中的列表与函数func中的列表相同,您只需更改函数即可返回列表id a在函数体内部使用。然后,将其与__defaults__中的列表([0]中的位置__defaults__)进行比较,您将看到它们是如何确实引用相同的列表实例的:

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

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

答案 7 :(得分:54)

防御Python的5分

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

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

  3. 实用性:正如Frederik Lundh在他的解释中指出的那样 "Default Parameter Values in Python"的{。} 当前行为对于高级编程非常有用。 (谨慎使用。)

  4. 足够的文档:在最基本的Python文档中, 该教程,该问题被大声宣布为 部分的第一个小节中的&#34;重要警告&#34; "More on Defining Functions"。 警告甚至使用粗体, 很少在标题之外应用。 RTFM:阅读精细手册。

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

答案 8 :(得分:46)

这种行为很容易解释为:

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

    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更改 - 对同一对象执行操作 - 并将其打印

答案 9 :(得分:33)

你问的是为什么:

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

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

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

答案 10 :(得分: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]

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

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 属性中。 (假设不应按惯例从此类或子类之外修改实例的私有属性。即_var1是私有属性)

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

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

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

答案 11 :(得分: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]

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

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

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

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

答案 12 :(得分:25)

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

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中,你没有这个好处。

答案 13 :(得分:25)

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

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

答案 14 :(得分:22)

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

def a(): return []

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

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

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

答案 15 :(得分:20)

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

这是否违反了&#34;最不惊讶的原则&#34;?

这种执行顺序经常让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 L

答案 16 :(得分:19)

使用无

的简单解决方法
>>> 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]

答案 17 :(得分:19)

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

  1. 分配尝试时的只读类属性的行为,以及
  2. 功能是对象(在接受的答案中解释清楚)。
  3. (2)的作用已在本主题中广泛讨论。 (1)可能是令人惊讶的因素,因为这种行为在来自其他语言时并非“直观”。

    Python中tutorial on classes描述了

    (1)。尝试将值分配给只读类属性:

      

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

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

    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)在本地作用域中可用{{1} }。
    • L后,分配无法在L = []成功,因为该属性是只读的。
    • (1) 在本地范围内创建一个名为foo.func_defs[0]的新局部变量 并用于函数调用的其余部分。 L因此foo.func_defs[0]。{/ 1>的未来调用保持不变

答案 18 :(得分: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

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

>>> id(a)
5347866528

>>> id(b)
5347866528

Per Brett Slatkin的“有效的Python:编写更好的Python的59种方法”,项目20:使用None和Docstrings指定动态默认参数(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]

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

答案 19 :(得分:17)

这里的解决方案是:

  1. 使用None作为默认值(或nonce object),然后打开它以在运行时创建值;或
  2. 使用lambda作为默认参数,并在try块中调用它以获取默认值(这是lambda抽象的用途)。
  3. 第二个选项很好,因为函数的用户可以传入一个可调用的,可能已经存在(例如type

答案 20 :(得分:16)

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

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

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

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

答案 21 :(得分:15)

当我们这样做时:

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]

答案 22 :(得分:15)

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

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

丑陋,但确实有效。

答案 23 :(得分:13)

可能是这样的:

  1. 有人正在使用所有语言/库功能,
  2. 在此处切换行为是不明智的,但
  3. 完全符合上述两个特征并仍然提出另一个观点:

    1. 这是一个令人困惑的功能,在Python中很不幸。
    2. 其他答案,或者至少其中一些答案要么分1和2而不是3分,要么分3分和低分1分和2分。但这三个都是真的。

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

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

答案 24 :(得分: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()

打印以下内容

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

答案 25 :(得分:8)

我认为这个问题的答案在于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]

[]是一个对象,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]

调用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)

每个按钮都可以保存一个独特的回调函数,该函数将显示i的不同值。我可以举例说明:

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

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

答案 26 :(得分:8)

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

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

  1. 您打算将参数修改为函数的副作用。在这种情况下,永远不会有意义拥有默认参数。唯一的例外是当您滥用参数列表以具有函数属性时,例如cache={},并且根本不会要求您使用实际参数来调用该函数。
  2. 您打算保留未经修改的参数,但您不小心 修改了它。这是一个错误,修复它。
  3. 您打算修改在函数内部使用的参数,但不希望修改在函数外部可见。在这种情况下,您需要创建参数的副本,无论它是否为默认值! Python不是一种按值调用的语言,所以它不会为你制作副本,你需要明确它。
  4. 问题中的示例可能属于类别1或3.奇怪的是它既修改了传递的列表又返回了它;你应该选择其中一个。

答案 27 :(得分:7)

只需将功能更改为:

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

答案 28 :(得分:4)

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=None):     # delay default until execution time
    parameter = [] if parameter is None else parameter
    ...

答案 29 :(得分:0)

其他所有答案都解释了为什么这实际上是一种不错的期望行为,或者为什么您不应该这样。 Mine适用于那些固执己见的人,他们想行使自己的权利将语言屈服于自己的意愿,而不是反过来。

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

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

现在让我们使用此装饰器重新定义功能:

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

装饰器可以进行调整以允许这样做,但是我们将其留给读者练习;)

答案 30 :(得分:-2)

有一种简单的方法可以了解为什么会发生这种情况。

Python在命名空间中从上到下执行代码。

“内部人”就是这一规则的体现。

此选择的原因是“让语言适合您的头脑”。所有奇怪的情况都倾向于简化为在命名空间中执行代码:默认不可变变量,嵌套函数,类(完成编译时会有一些修补),自变量等。类似地,可以使用简单语法编写复杂语法:a.foo(...)只是a.lookup('foo').__call__(a,...)。这适用于列表理解;装饰工;元类和更多。这使您几乎可以完美地看到奇怪的角落。语言适合您的需求。

您应该坚持下去。学习Python对该语言有一定的了解,但是很轻松。这是我使用过的唯一语言,您越看待极端情况,它就会变得越简单。

保持黑客入侵!记笔记。

对于您的特定代码,过于详细:

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

foo()

是一个语句,等效于:

  1. 开始制作代码对象。
  2. 现在就进行翻译,(a=[])[]是参数a的默认值。它是列表类型,就像[]一样。
  3. :之后的所有代码编译为Python字节码,并将其粘贴在另一个列表中。
  4. 在“ 代码”字段中使用参数和代码创建可调用字典
  5. 将可调用对象添加到“ foo”字段中的当前名称空间。

然后,转到下一行foo()

  1. 这不是保留字,所以look it up in the namespace
  2. 调用该函数,它将使用列表作为默认参数。开始在其名称空间中执行其字节码。
  3. append不会创建新列表,因此旧列表会被修改。

答案 31 :(得分:-2)

讨论尚未提及可变和不可变默认参数的范围差异,当它们之前被声明为 vars 时。

先前声明为 vars 的默认参数的值变得可访问,就好像它们被声明为全局一样。

然而,如果函数试图改变它们(甚至在函数的后期),它们就不再是全局的。如前所述,只要不改变它们,您就可以自由更改它们。

它基于与常规最小惊讶相同的逻辑,但是它也会影响范围,因此熟悉它很重要。

不改变默认参数值的函数可以自由访问它们:

global_list = [1]
global_int = 1
def foo2(a=global_list,b=global_int):
    print('global_list:')
    print(global_list) # implicity global
    print('global_int:')
    print(global_int) # implicity global


>>> foo2()
global_list:
[1]
global_int:
1
>>> global_list = [2]
>>> global_int = 2
>>> foo2()
global_list:
[2]
global_int:
2

然而,该函数无法从内部更改它们,错误消息可能会令人困惑,因为它表现得好像根本不知道这个变量:

def foo3(a=global_list,b=global_int):
    print('global_list:')
    print(global_list) # implicity global
    global_list.remove(2) # yes, i'm "cheating"
    global_list.append(3) 
    print(global_list) # implicity global
    print('global_int:')
    print(global_int) # local
    print('the error is in the previous line is because this next line assignment makes global_int a local var')
    global_int = 3
    print(global_int)


>>> foo3()
global_list:
[1]
global_int:
Traceback (most recent call last)...
UnboundLocalError: local variable 'global_int' referenced before assignment

代码示例:https://onlinegdb.com/BkB1Unb1u

答案 32 :(得分:-4)

建筑

在函数调用中分配默认值是代码气味。

def a(b=[]):
    pass

这是功能的签名,没有任何好处。不仅仅是因为其他答案所描述的问题。我不会在这里进去。

这个功能旨在做两件事。创建一个新列表,并执行一项功能,最有可能在所述列表上。

当我们从清洁代码实践中学习时,做两件事的函数就是坏函数。

使用多态来解决这个问题,我们会扩展python列表或在类中包装一个,然后在它上面执行我们的函数。

但是等你说,我喜欢我的单行。

嗯,猜猜看。代码不仅仅是一种控制硬件行为的方法。 这是一种方式:

  • 与其他开发人员沟通,处理相同的代码。

  • 能够在出现新要求时更改硬件的行为。

  • 在两年后再次拿起代码进行上述更改后,能够理解程序的流程。

不要为自己留下时间炸弹以便以后再接。

将这个功能分成它所做的两件事,我们需要一个类

class ListNeedsFives(object):
    def __init__(self, b=None):
        if b is None:
            b = []
        self.b = b

    def foo():
        self.b.append(5)

执行
a = ListNeedsFives()
a.foo()
a.b

为什么这比将所有上述代码混合到一个函数中更好。

def dontdothis(b=None):
    if b is None:
        b = []
    b.append(5)
    return b

为什么不这样做?

除非您的项目失败,否则您的代码将继续存在。很可能你的功能不仅仅是这个。制作可维护代码的正确方法是将代码分成具有适当限制范围的原子部分。

对于完成面向对象编程的任何人来说,类的构造函数是一个非常普遍认可的组件。在构造函数中放置处理列表实例化的逻辑会使认知负载理解代码所做的更小。

方法foo()不会返回列表,为什么不呢?

在返回独立列表时,您可以认为按照您的意愿行事是安全的。但它可能不是,因为它也由对象a共享。强制用户将其引用为a.b,提醒他们列表所属的位置。任何想要修改a.b的新代码自然会放在它所属的类中。

def dontdothis(b=None):签名函数没有这些优点。