关于python:”Least Astonishmen”和可变的默认参数

“Least Astonishment” and the Mutable Default Argument

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

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

python新手希望这个函数总是返回一个只有一个元素的列表:[5]。结果是非常不同,非常惊人(对于新手来说):

1
2
3
4
5
6
7
8
9
>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

我的一位经理曾经第一次遇到这个特性,他称之为语言的"戏剧性设计缺陷"。我回答说,这一行为有一个潜在的解释,如果你不理解其内部结构,这确实是非常令人困惑和意想不到的。但是,我无法(对自己)回答以下问题:在函数定义而不是在函数执行时绑定默认参数的原因是什么?我怀疑经验丰富的行为是否有实际用途(谁真的在C中使用了静态变量,而没有滋生虫子?)

编辑:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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中的函数是一流的对象,而不仅仅是一段代码。

一旦你这样想,它就完全有意义了:一个函数是一个正在根据其定义进行评估的对象;默认参数是一种"成员数据",因此它们的状态可能会从一个调用改变到另一个,就像在任何其他对象中一样。

在任何情况下,effbot在python的默认参数值中都能很好地解释这种行为的原因。我发现它非常清晰,我真的建议阅读它,以便更好地了解对象的工作原理。


假设您有以下代码

1
2
3
4
fruits = ("apples","bananas","loganberries")

def eat(food=fruits):
    ...

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

但是,在代码的后面,我会做一些类似的事情

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

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

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

1
2
3
4
5
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会这样做)n不允许将其可变的内置数据类型用作字典键)。

您的例子是一个很好的例子,在这个例子中,新来的python用户会感到惊讶和被咬。但我认为,如果我们"修复"这个问题,那么只会造成一个不同的情况,相反,他们会被咬,而且一个会更不直观。此外,在处理可变变量时,情况总是如此;您总是遇到这样的情况:根据编写的代码,某人可以直观地期望一个或相反的行为。

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


AFAICS还没有人发布文档的相关部分:

Default parameter values are evaluated 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 [...]


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

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

1
a = []

您希望得到一个由a引用的新列表。

为什么a=[]应该在

1
def x(a=[]):

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

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

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

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

我知道,我知道:这是一个终结。或者,python可以提供一个关键字来强制定义时间绑定:

1
def x(static a=b):


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

比较一下:

1
2
3
4
5
class BananaBunch:
    bananas = []

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

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

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

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

也就是说,它应该在任何优秀的Python教程中都有突出的特点。因为正如你所说,每个人迟早都会遇到这个问题。


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

1。性能

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

如果使用了调用时间计算,那么每次使用函数时都会在没有参数的情况下调用昂贵的函数。您要么在每次调用上付出昂贵的代价,要么需要在外部手动缓存值,污染您的名称空间并添加冗长的内容。

2。强制绑定参数

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

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

这将返回返回0、1、2、3…分别。如果行为改变了,它们将把i绑定到i的调用时间值,这样您将得到一个所有返回9的函数列表。

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

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

三。反省

考虑代码:

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

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

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

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

现在,假设违约行为可以改变,这相当于:

1
2
3
4
5
6
_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=[]

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


你为什么不反省一下?

我真的很惊讶没有人对可调用文件进行了由python(23apply)提供的深入的自省。

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

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

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

所以,让我们做一些内省,前后检查列表如何在函数对象中展开。我用Python 3.x来表示这个,对于python 2同样适用(在python 2中使用__defaults__func_defaults;是的,两个名称表示相同的东西)。

执行前的函数:

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

在python执行这个定义之后,它将使用任何指定的默认参数(这里是a = [])并将它们填充到函数对象的__defaults__属性中(相关部分:callables):

1
2
>>> func.__defaults__
([],)

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

执行后的函数:

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

1
>>> func()

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

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

惊讶的?对象内的值更改!对函数的连续调用现在只需附加到嵌入的list对象:

1
2
3
>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

所以,你已经知道了,这个"缺陷"发生的原因,是因为默认参数是函数对象的一部分。这里没有什么奇怪的事情,只是有点奇怪。

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

1
2
3
4
def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

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

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

1
2
3
4
5
6
>>> def func(a = []):
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

所有的一切都有内省的力量!

*要验证在编译函数期间python是否计算默认参数,请尝试执行以下操作:

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

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


Python防御5分

  • 简单性:行为简单如下:大多数人只会掉进这个陷阱一次,而不是几次。

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

  • 有用性:正如弗雷德里克·伦德在他的解释中指出的那样在"python中的默认参数值"中,当前行为对于高级编程非常有用。(谨慎使用。)

  • 足够的文档:在最基本的python文档中,教程中,这个问题被大声宣布为第一节中的"重要警告""关于定义函数的更多信息"。警告甚至用黑体字,它很少应用于标题之外。RTFM:阅读精细手册。

  • 元学习:陷入陷阱实际上是有帮助的时刻(至少如果你是一个反思性的学习者)因为你随后会更好地理解这一点上面的"一致性",这将教你很多关于Python的知识。


  • 这种行为很容易解释为:

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

    1
    2
    3
    4
    5
    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更改—在同一对象上执行操作—并打印

  • 你要问的是为什么:

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

    内部不等同于:

    1
    2
    3
    4
    5
    6
    7
    8
    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),我们将忽略它。

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

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


    1)"可变默认参数"的问题通常是一个特殊的例子,说明:所有有此问题的函数在实际参数上也会遇到类似的副作用问题,这违背了函数式编程的规则,通常是不可考虑的,应该同时固定两者。

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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,然后再对副本执行任何操作。

    1
    2
    3
    4
    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()更彻底地复制(如果可变对象由可变对象组成,后者很有用)。有些对象基本上是基于副作用的,比如"文件"对象,不能通过复制来有意义地复制。复印

    类似so问题的示例问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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]

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

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

    2)只有当对实际参数的副作用是必需的,而对默认参数是不需要的,那么有用的解决方案是def ...(var1=None):if var1 is None:var1 = []更多。

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


    这实际上与默认值没有任何关系,除了在编写具有可变默认值的函数时,它通常会出现一种意外的行为。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    >>> 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中,也不管是否涉及默认参数。

    如果在计算某个东西的过程中需要破坏性地操纵一个局部临时变量,并且需要从一个参数值开始操纵,那么就需要制作一个副本。


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

    1
    2
    3
    4
    5
    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

    1
    2
    3
    4
    5
    0 LOAD_GLOBAL              0 (print_tuple)
    3 CALL_FUNCTION            0
    6 POP_TOP
    7 LOAD_CONST               0 (None)
    10 RETURN_VALUE

    #2

    1
    2
    3
    4
    5
    6
     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

    I doubt the experienced behavior has a practical use (who really used static variables in C, without breeding bugs ?)

    如您所见,在使用不可变的默认参数时有一个性能优势。如果它是一个经常调用的函数,或者默认参数需要很长时间来构造,那么这可能会有所不同。另外,请记住,python不是c。在c中,有相当多的自由常数。在python中,您没有这个好处。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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


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

    1
    2
    3
    4
    def a(): return []

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

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

    不过,我同意,当您尝试使用默认构造函数时,这是一个很好的方法。


    python:可变的默认参数

    默认参数在函数编译为函数对象时进行计算。当被函数使用时,该函数多次使用时,它们是并保持不变的对象。

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

    它们之所以保持变异,是因为它们每次都是同一个物体。

    等效代码:

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

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

    几乎完全等同于:

    1
    2
    3
    4
    5
    6
    _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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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
    2
    3
    4
    5
    6
    7
    8
    9
    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用户的常规说明:

    但这就是为什么对新用户通常的指令是创建这样的默认参数:

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

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

    正如控制流的教程部分所说:

    If you don’t want the default to be shared between subsequent calls,
    you can write the function like this instead:

    1
    2
    3
    4
    5
    def f(a, L=None):
        if L is None:
            L = []
        L.append(a)
        return L

    使用无的简单解决方案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    >>> 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]

    如果考虑到以下因素,这种行为并不奇怪:

  • 尝试赋值时只读类属性的行为,以及
  • 函数是对象(在公认的答案中解释得很好)。
  • (2)的作用在这个线程中已经被广泛地覆盖。(1)很可能是引起惊异的因素,因为这种行为在来自其他语言时不是"直观的"。

    (1)在关于类的Python教程中进行了描述。试图为只读类属性赋值时:

    ...all variables found outside of the innermost scope are
    read-only (an attempt to write to such a variable will simply create a
    new local variable in the innermost scope, leaving the identically
    named outer variable unchanged).

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

    1
    2
    3
    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中的代码执行之间一直存在。

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

    1
    2
    3
    4
    5
    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时,foo.func_defs[0]保持不变。

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

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

    错误的方法(可能是…):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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来验证它们是同一个对象:

    1
    2
    3
    4
    5
    >>> id(a)
    5347866528

    >>> id(b)
    5347866528

    根据BrettSlatkin的"有效的python:59编写更好的python的特定方法",第20项:使用None和docstrings指定动态默认参数(第48页)

    The convention for achieving the desired result in Python is to
    provide a default value of None and to document the actual behaviour
    in the docstring.

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

    首选方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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]

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


    这里的解决方案是:

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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    singleton = None

    def use_singleton():
        global singleton

        if singleton is None:
            singleton = _make_singleton()

        return singleton.use_me()

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

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

    我使用它来实例化访问外部资源的客户机类,以及创建用于内存化的dict或列表。

    因为我不认为这种模式是众所周知的,所以我确实做了一个简短的评论来防止将来的误解。


    当我们这样做时:

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

    …如果调用方不传递a的值,我们将参数a赋给一个未命名的列表。

    为了使讨论更简单,我们暂时为未命名的列表命名。埃多克斯1〔1〕怎么样?

    1
    2
    def foo(a=pavlo):
       ...

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

    如果pavlo是可变的(可修改的),而foo最终修改了它,那么下次调用foo时,如果没有指定a我们会注意到这一效果。

    所以这就是你看到的(记住,pavlo被初始化为[]):

    1
    2
     >>> foo()
     [5]

    现在,pavlo是[5]。

    再次调用foo()修改pavlo

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

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

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

    因此,pavlo仍然是[5, 5]

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

    您可以通过替换对象来绕过这个问题(因此也可以用范围来绑定):

    1
    2
    3
    4
    def foo(a=[]):
        a = list(a)
        a.append(5)
        return a

    很难看,但很管用。


    可能是真的:

  • 有人正在使用每种语言/库功能,并且
  • 在这里改变行为是不明智的,但是
  • 它完全一致地保留了上述两个特性,并且仍然提出了另一个观点:

  • 这是一个令人困惑的特性,在Python中这是不幸的。
  • 其他答案,或者至少其中的一些,要么得到1分和2分,但不是3分,要么得到3分和1分和2分。但这三个都是真的。

    这可能是真的,在中游切换马会要求显著的中断,并且通过更改python来直观地处理stefano的开头片段可能会产生更多的问题。而且,了解Python内部结构的人能够很好地解释后果的雷区,这可能是真的。然而,

    现有的行为不是Python式的,而Python之所以成功,是因为很少有语言违反了在这附近任何地方都不令人吃惊的原则。这是一个真正的问题,是否是明智的根除它。这是一个设计缺陷。如果你试图通过追踪行为来更好地理解语言,我可以说C++做了所有这些和更多;你通过导航,例如微妙的指针错误学到了很多。但这并不是Python式的:那些足够关心Python并坚持面对这种行为的人是那些被语言所吸引的人,因为与其他语言相比,Python的惊喜要少得多。当达布勒和好奇的人惊讶于花了这么少的时间让某个东西工作时,他们会变成pythonistas——不是因为一个设计fl——我的意思是,隐藏的逻辑难题——这违背了程序员的直觉,他们被python吸引,因为它只是工作。


    这个"错误"给了我很多加班时间!但我开始看到它的潜在用途(但我还是希望它在执行时出现)

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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 '
    '
    .join(errors)

    main()

    打印以下内容

    1
    2
    3
    4
    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(),通过append方法将列表[]更改为1。但是请注意,列表对象只有一个副本,这个对象现在变成1。当运行第二个foo()时,effbot网页所说的(项目不再被评估)是错误的。a被评估为列表对象,尽管现在对象的内容是1。这是通过引用传递的效果!FOO(3)的结果可以用同样的方法很容易得到。

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

    第2号

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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号

    1
    2
    3
    4
    5
    6
    7
    def foo(x, items=[]):
        items.append(x)
        return items

    foo(1)    # returns [1]
    foo(2,[]) # returns [2]
    foo(3)    # returns [1,3]

    对foo(1)的调用使items指向一个地址为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的代码是正确的:

    1
    2
    3
    4
    for i in range(10):
        def callback():
            print"clicked button", i
        UI.Button("button %s" % i, callback)

    每个按钮都可以有一个不同的回调函数,显示不同的i值。我可以举一个例子来说明这一点:

    1
    2
    3
    4
    5
    x=[]
    for i in range(10):
        def callback():
            print(i)
        x.append(callback)

    如果我们执行x[7](),我们会得到预期的7,x[9]()会得到9,另一个值是i


    这不是设计缺陷。任何一个在这上面绊倒的人都在做错事。

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

  • 您打算将参数修改为函数的副作用。在这种情况下,使用默认参数是没有意义的。唯一的例外是,当您滥用参数列表来具有函数属性时,例如cache={},您就不会期望使用实际参数调用函数。
  • 你打算不修改这个论点,但你不小心修改了它。那是个虫子,修好它。
  • 您打算修改在函数内部使用的参数,但不希望修改在函数外部可见。在这种情况下,您需要复制参数,不管它是否是默认值!python不是按值调用的语言,因此它不会为您复制,您需要对它进行明确说明。
  • 问题中的例子可能属于1类或3类。奇怪的是,它同时修改传递的列表并返回它;您应该选择其中一个。


    只需将函数更改为:

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

    tldr:define时间默认值是一致的,并且更具表现力。

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

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

    def name部分必须在定义的范围内进行评估,毕竟我们希望name在那里可用。只在函数本身内部计算函数将使其不可访问。

    由于parameter是一个常数名,我们可以在与def name同时"评估"它。它还具有这样的优点:它生成的函数具有一个名为name(parameter=...):的已知签名,而不是一个空的name(...):

    现在,什么时候评估default

    一致性已经表示"在定义上":def ():中的其他所有内容也在定义上得到了最好的评估。推迟部分时间将是令人吃惊的选择。

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

    1
    2
    3
    4
    5
    6
    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
        ...

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    >>> def a():
    >>>    print"a executed"
    >>>    return []
    >>> x =a()
    a executed
    >>> def b(m=[]):
    >>>    m.append(5)
    >>>    print m
    >>> b(x)
    [5]
    >>> b(x)
    [5, 5]


    每一个其他的答案都解释了为什么这实际上是一个好的和想要的行为,或者为什么你无论如何都不应该需要它。我的是那些顽固的人,他们想行使他们的权利,使语言屈从于他们的意志,而不是相反的方式。

    我们将使用一个修饰符来"修复"这个行为,该修饰符将复制默认值,而不是为每个保留在默认值的位置参数重用相同的实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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

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

    1
    2
    3
    4
    5
    6
    7
    @sanify
    def foo(a=[]):
        a.append(5)
        return a

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

    对于采用多个参数的函数来说,这尤其简单。比较:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 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

    具有

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

    重要的是要注意,如果您尝试使用关键字args,上面的解决方案会中断,例如:

    1
    foo(a=[4])

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


    建筑

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

    1
    2
    def a(b=[]):
        pass

    这是一个不起作用的函数的签名。不仅仅是因为其他答案所描述的问题。我不想在这里谈这个。

    这个功能的目的是做两件事。创建一个新的列表,并执行一个功能,最有可能出现在该列表中。

    做两件事的函数是坏函数,正如我们从干净的代码实践中学习到的。

    利用多态性来解决这个问题,我们将扩展python列表或将其包装在一个类中,然后对其执行我们的函数。

    但是等等,你说,我喜欢我的一句台词。

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

    • 与其他开发人员进行通信,使用相同的代码。

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

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

    不要把定时炸弹留给你自己以后取。

    把这个函数分成两个部分,我们需要一个类

    1
    2
    3
    4
    5
    6
    7
    8
    class ListNeedsFives(object):
        def __init__(self, b=None):
            if b is None:
                b = []
            self.b = b

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

    被执行

    1
    2
    3
    a = ListNeedsFives()
    a.foo()
    a.b

    为什么这比将上面所有的代码混合成一个函数要好呢?

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

    为什么不这样做?

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

    类的构造函数对于任何进行过面向对象编程的人来说都是一个非常常见的组件。将处理列表实例化的逻辑放在构造函数中,可以使理解代码所做工作的认知负载变小。

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

    在返回一个独立的列表时,您可以假设做任何您想做的事情都是安全的。但它可能不是,因为它也由对象a共享。强迫用户将其称为a.b,以提醒他们列表所属的位置。任何想要修改a.b的新代码都将自然地放在它所属的类中。

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