长时间修改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行在部分绑定的意义上是“混合的” (函数对象的)将在定义时发生,部分(默认参数的分配)将在函数调用时发生。

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


#1楼

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

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

(2)的作用已在该线程中广泛讨论。 (1)可能是令人惊讶的原因,因为这种行为在来自其他语言时不是“直观”的。

(1)在有关类的Python 教程中进行了描述。 在尝试为只读类属性分配值时:

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

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

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

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

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该变量也称为L ,用于其余的函数调用。 foo.func_defs[0]因此对于以后的foo调用保持不变。

#2楼

假设您有以下代码

fruits = ("apples", "bananas", "loganberries")def eat(food=fruits):...

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

但是,假设稍后在代码中,我做类似

def some_random_function():global fruitsfruits = ("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当前的方法:定义函数时会评估默认函数参数,而该对象始终是默认对象。 我想他们可以使用空列表作为特殊情况,但是这种特殊的大小写会引起更多的惊讶,更不用说向后不兼容了。


#3楼

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

比较一下:

class BananaBunch:bananas = []def addBanana(self, banana):self.bananas.append(banana)

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

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

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

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


#4楼

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

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

所以:

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

#5楼

您要问的是为什么这样:

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

在内部不等同于此:

def func(a=None, b = None):a_default = lambda: []b_default = lambda: 2def 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)的情况外,我们将忽略它。

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

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


#6楼

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

def print_tuple(some_tuple=(1,2,3)):print some_tupleprint_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            19 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

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

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


#7楼

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

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

a = []

你希望得到通过引用新的列表a

为什么在a=[]

def x(a=[]):

在函数定义而不是调用上实例化一个新列表? 就像您要问“如果用户不提供参数,则实例化一个新列表并像调用方产生的那样使用它”。 我认为这是模棱两可的:

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

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

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

我知道,我知道:那是一个封闭。 另外,Python可以提供一个关键字来强制定义时间绑定:

def x(static a=b):

#8楼

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

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 valuedef foo(a=_undefined, b=_undefined, c=_undefined)if a is _undefined: a='test'if b is _undefined: b=100if c is _undefined: c=[]

但是,我们失去了自省的能力,无法看到默认参数 。 由于尚未构造对象,因此,如果不实际调用函数,就无法拥有它们。 我们最好的办法是存储源代码,并将其作为字符串返回。


#9楼

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

def a(): return []def b(x=a()):print x

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

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


#10楼

可能确实是:

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

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

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

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

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

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


#11楼

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

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


#12楼

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

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

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


#13楼

1)所谓的“可变默认参数”问题通常是一个特殊的示例,它表明:
“所有具有此问题的功能在实际参数上也遭受类似的副作用问题 ,”
这违背了通常不受欢迎的函数式编程规则,应该将两者固定在一起。

例:

def foo(a=[]):                 # the same problematic functiona.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 copy输入对象,然后再执行复制操作。

def foo(a=[]):a = a[:]     # a copya.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.copy(any_object)更彻底地copy.deepcopy()如果可变对象是由可变对象组成的,后者很有用)。 有些对象从根本上是基于副作用的,例如“文件”对象,并且不能通过复制有意义地进行复制。 复制中

类似的SO问题的示例问题

class Test(object):            # the original problematic classdef __init__(self, var1=[]):self._var1 = var1somevar = [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]

不应将其保存在此函数返回的实例的任何公共属性中。 (假设实例的私有属性不应按照惯例从此类或子类的外部进行修改。即_var1是私有属性)

结论:
输入参数对象不应就地修改(突变),也不应将其绑定到函数返回的对象中。 (如果我们更喜欢强烈建议没有副作用的编程。请参见Wiki上的“副作用” (在此上下文中,前两段是相关内容)。)

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

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


#14楼

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

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

丑陋,但是行得通。


#15楼

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

#16楼

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

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

def example(errors=[]):# statements# Something went wrongmistake = Trueif mistake:tryToFixIt(errors)# Didn't work.. let's try againtryToFixItAnotherway(errors)# This time it workedreturn errorsdef 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

#17楼

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

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

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

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

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

====== 2号========

def foo(x, items=None):if items is None:items = []items.append(x)return itemsfoo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]是一个对象, None对象也是如此(前者是可变的,而后者是不可变的。但是可变性与问题无关)。 空间中没有一个地方,但我们知道它在那里,那里只有一个副本。 因此,每次调用foo时,项都会被评估为“无”(与之对应的答案是只被评估一次),显然,该引用(或地址)为“无”。 然后在foo中,item更改为[],即指向另一个具有不同地址的对象。

====== 3号=======

def foo(x, items=[]):items.append(x)return itemsfoo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

foo(1)的调用使项指向具有地址的列表对象[],例如11111111。在续集的foo函数中,列表的内容更改为1 ,但地址未更改,仍然为11111111然后foo(2,[])来了。 尽管在调用foo(1)时,foo(2,[])中的[]与默认参数[]的内容相同,但是它们的地址却不同! 由于我们显式提供了参数,因此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", iUI.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


#18楼

当我们这样做时:

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

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

为了简化讨论,让我们暂时为未命名列表命名。 pavlo怎么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]

#19楼

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

singleton = Nonedef use_singleton():global singletonif 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()

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

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


#20楼

话题已经很忙了,但是根据我在这里所读的内容,以下内容帮助我意识到了它在内部的工作方式:

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

#21楼

捍卫Python的5分

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

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

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

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

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


#22楼

只需将功能更改为:

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

#23楼

我将演示将默认列表值传递给函数的替代结构(与字典同样有效)。

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

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

def foo(list_arg=[5]):return list_arga = 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:59种编写更好的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_arga = foo()
a.append(6)
>>> a
[5, 6]b = foo()
b.append(7)
>>> b
[5, 7]c = foo([10])
c.append(11)
>>> c
[10, 11]

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


#24楼

你为什么不自省?

我真的惊讶,没有人对可调用对象执行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 = [] ), 并将其__defaults__函数对象的__defaults__属性中 (相关部分: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 aif 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

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


#25楼

Python:可变默认参数

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

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

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

等效代码:

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

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

几乎完全等同于此:

_a_list = [] # create a list in the globalsdef 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 lprint('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单例作为哨兵对象来告诉函数我们是否获得了默认值以外的参数。 如果没有参数,则实际上我们想使用一个新的空列表[]作为默认值。

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

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

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

#26楼

这不是设计缺陷 。 绊倒这个的人做错了什么。

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

  1. 您打算修改参数作为函数的副作用。 在这种情况下, 没有默认参数是没有意义的 。 唯一的例外是,当您滥用参数列表以具有函数属性时,例如cache={} ,根本就不会期望使用实际参数来调用函数。
  2. 您打算保留该参数不变,但您无意中对其做了修改。 那是一个错误,修复它。
  3. 您打算修改在函数内部使用的参数,但是并不希望修改在函数外部可见。 在这种情况下,无论是否为默认值,都需要复制该参数! Python不是按值调用的语言,因此它不能为您创建副本,您需要对其进行明确说明。

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


#27楼

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 timeparameter = default if parameter is None else parameter...

#28楼

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

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

import inspect
from copy import copydef sanify(function):def wrapper(*a, **kw):# store the default valuesdefaults = inspect.getargspec(function).defaults # for python2# construct a new argument listnew_args = []for i, arg in enumerate(defaults):# allow passing positional argumentsif i in range(len(a)):new_args.append(a[i])else:# copy the valuenew_args.append(copy(arg))return function(*new_args, **kw)return wrapper

现在,让我们使用此装饰器重新定义函数:

@sanify
def foo(a=[]):a.append(5)return afoo() # '[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])

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


#29楼

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

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

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


#30楼

这里的解决方案是:

  1. 使用None作为默认值(或nonce object ),并在运行时将其打开以创建您的值; 要么
  2. 使用lambda作为默认参数,然后在try块中调用它以获取默认值(这是lambda抽象用于的事情)。

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

“最少惊讶”和可变默认参数相关推荐

  1. python默认参数 可变对象_当心Python函数可变默认参数(list,set,dict…)的陷阱

    绝大多数情况下,Python是一个干净具有一致性的语言.然而,有些少数情况会让初学者感到困惑.其中有些情况是有意识的但会成为潜在的莫名其妙,而有些可以说是语言赘肉.下面我们看看使用可变默认参数(Mut ...

  2. python函数之可变默认参数

    文章目录 问题剖析 元组的使用 一个 Python Bug 干倒了估值 1.6 亿美元的公司 今天在CSDN首页看到这篇文章,不仅感概: 水能载舟,亦能覆舟 作为一家仰仗技术出身的公司,最终却因为技术 ...

  3. python默认参数 可变对象_最小经验原则(POLA)与可变默认参数

    题目 任何长时间学习Python的人都会遇到下面的问题. def foo(a=[]): a.append(5) return a Python初学者期望这个函数总是会返回一个只包含一个元素的列表:[5 ...

  4. Python进阶-函数默认参数,特别是参数传递为空列表

    这两天遇到函数默认参数的bug,在互联网上好好总结了一下: 如非特别说明,下文均基于Python3 一.默认参数 python为了简化函数的调用,提供了默认参数机制: def pow(x, n = 2 ...

  5. 软件测试学习 之 Python 函数默认参数

    转载说明 作者:珞樱缤纷 出处:博客园 博文:Python进阶-函数默认参数 Python进阶-函数默认参数 写在前面 如非特别说明,下文均基于Python3 一.默认参数 python为了简化函数的 ...

  6. Python 精选笔试面试习题—类继承、方法对象、包管理、闭包、可变类型作为默认参数、列表引用、sort与sorted、 append 和 extend、深拷贝和浅拷贝

    1. 类继承 如下代码 class A(object):def show(self):print 'This is calss A'class B(A):def show(self):print 'T ...

  7. python中lambda 表达式(无参数、一个参数、默认参数、可变参数(*args、**kwargs)、带判断的lambda、列表使用lambda)

    如果⼀个函数有⼀个返回值,并且只有⼀句代码,可以使⽤ lambda简化. lambda语法: lambda 参数列表 : 表达式 注意: lambda表达式的参数可有可⽆,函数的参数在lambda表达 ...

  8. python函数用法详解2(变量的作用域(全局变量、局部变量)、共享全局变量、函数返回值、函数的参数(位置参数、关键字参数、默认参数、不定长参数)、拆包、交换变量值、引用、可变和不可变类型)

    1. 变量作⽤域         变量作⽤域指的是变量⽣效的范围,主要分为两类:局部变量和全局变量. 局部变量         定义在函数体内部的变量,即只在函数体内部⽣效. def testA(): ...

  9. python可变参数和关键字参数位置_python的位置参数、默认参数、关键字参数、可变参数区别...

    一.位置参数 调用函数时根据函数定义的参数位置来传递参数. #!/usr/bin/env python # coding=utf-8def print_hello(name, sex): sex_di ...

最新文章

  1. 10万人的1000万张图像,微软悄然删除最大公开人脸数据集
  2. Java IO 总结图
  3. linux ssh 报错 Failed to start OpenSSH Server daemon
  4. Zynq的AMP开发注意事项之sdk_repo
  5. Java线程详解(11)-线程池
  6. lacp可以在access接口吗_【基础】防火墙接口类型全介绍
  7. 002.ICMP--拼接ICMP包,实现简单Ping程序(原始套接字)
  8. Debian 9.6.0 + OpenMediaVault 4.x : U盘作系统盘时遇到的问题
  9. boa服务器实现温湿度显示,SMT车间温湿度分布式远程监控系统的设计
  10. linux程序运行耗时shell脚本running_time.sh
  11. react 引入轮播插件_React.js实现轮播图
  12. 国际电信联盟:3GPP系标准成为唯一被认可的5G标准
  13. java 内存分配参数_浅谈JAVA内存分配与参数传递
  14. Python安装时报缺少DLL的解决办法
  15. 使用双向链表构建二叉树_LeetCode-109 有序链表转换二叉搜索树
  16. CentOS 下编译安装AliSQL
  17. 51汇编——矩阵键盘
  18. ulead gif animator 5.11中文破解版|ulead gif animator绿色中文破解版下载 v5.11
  19. oracle rac告警,oracle11.2.0.4 RAC 日志总有告警
  20. docer启动一个容器时的过程

热门文章

  1. LeakCanary 源码解析
  2. Please select Android SDK
  3. 如何在BIOS里设置定时关机?
  4. 项目四-用循环求(2)
  5. android application常见错误
  6. android系统默认铃声,Android系统修改默认铃声
  7. python读取文件中的数据为二维数组变量_Numpy 多维数据数组的实现
  8. View是如何被添加到屏幕窗口上的
  9. DOM-based XSS Test Cases
  10. 一次SQLSERVER触发器编写感悟