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]。结果却大不相同,而且非常惊人(对于新手而言): 我的一位经理曾经第一次接触到这个特性,并称之为语言的“戏剧性的设计缺陷”。我回答说,这种行为有一个潜在的解释,如果你不理解其内部,这确实是非常令人费解和意外的。但是,我无法回答(对自己)以下问题:为什么要在函数定义而不是在函

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

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

Python新手希望此函数总是返回一个只有一个元素的列表:
[5]
。结果却大不相同,而且非常惊人(对于新手而言):

我的一位经理曾经第一次接触到这个特性,并称之为语言的“戏剧性的设计缺陷”。我回答说,这种行为有一个潜在的解释,如果你不理解其内部,这确实是非常令人费解和意外的。但是,我无法回答(对自己)以下问题:为什么要在函数定义而不是在函数执行时绑定默认参数?我怀疑这种有经验的行为是否有实际用途(谁真的在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
有效地绑定到指定的默认值,而函数未定义,这将带来一个严重的缺陷:
def
行将是“混合”的,从这个意义上讲,(函数对象的)绑定的一部分将在定义时发生,而另一部分将在定义时发生(默认参数的赋值)在函数调用时


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

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

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

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

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

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

stringbuffers=newstringbuffer(“你好,世界!”);
映射计数=新的HashMap();
计数。放置(s,5);
s、 附加(“!!!!”);
System.out.println(counts.get(s));//这行吗?
现在,我的地图是在放入地图时使用
StringBuffer
键的值,还是通过引用存储键?不管怎样,都有人感到惊讶;或者是试图使用与放入对象的值相同的值从
map
中取出对象的人,或者是似乎无法检索的人它们的对象,即使它们使用的键与用于将其放入映射的对象完全相同(这实际上就是Python不允许其可变内置数据类型用作字典键的原因)

你的例子是一个很好的例子,Python新手会感到惊讶和被咬。但是我认为如果我们“修复”这,那么那只会造成一种不同的情况,他们会被咬,而那一种情况会更不直观。此外,在处理可变变量时总是这样;你总是会遇到这样的情况:根据他们编写的代码,有人可以直观地预期一种或相反的行为


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

好吧,原因是q简单地说,绑定是在代码执行时完成的,函数定义是在函数定义时执行的

比较一下:

class BananaBunch:
    bananas = []

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

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

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


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

这种行为很容易解释为:

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

  • a
    不会更改-每次分配调用都会创建新的int对象-打印新对象
  • b
    不变-新数组是根据默认值生成并打印的
  • c
    changes-对同一对象执行操作-并打印

  • 你要问的是为什么会这样:

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

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

    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()
    def print_tuple(some_tuple=(1,2,3)):
        print some_tuple
    
    print_tuple()        #1
    print_tuple((1,2,3)) #2
    
    0 LOAD_GLOBAL              0 (print_tuple)
    3 CALL_FUNCTION            0
    6 POP_TOP
    7 LOAD_CONST               0 (None)
    10 RETURN_VALUE
    
     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
    
    a = []
    
    def x(a=[]):
    
    def x(a=datetime.datetime.now()):
    
    b = datetime.datetime.now()
    def x(a=b):
    
    def x(static a=b):
    
    def foo(arg=something_expensive_to_compute())):
        ...
    
    funcs = [ lambda i=i: i for i in range(10)]
    
    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.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 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]
    
    def foo(a=[]):
        a.append(5)
        return a
    
    def foo(a, L=None):
        if L is None:
            L = []
        L.append(a)
        return L
    
    def whats_on_the_telly(penguin=None):
        if penguin is None:
            penguin = []
        penguin.append("property of the zoo")
        return penguin
    
    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]"
    
    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]
    
    def foo(a=[]):
        a = list(a)
        a.append(5)
        return a
    
    >>> 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
    
    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]
    
    def foo(x, items=[]):
        items.append(x)
        return items
    
    foo(1)    # returns [1]
    foo(2,[]) # returns [2]
    foo(3)    # returns [1,3]
    
    for i in range(10):
        def callback():
            print "clicked button", i
        UI.Button("button %s" % i, callback)
    
    x=[]
    for i in range(10):
        def callback():
            print(i)
        x.append(callback) 
    
    def foo(a=[]):
        ...
    
    def foo(a=pavlo):
       ...
    
     >>> foo()
     [5]
    
    >>> foo()
    [5, 5]
    
    >>> ivan = [1, 2, 3, 4]
    >>> foo(a=ivan)
    [1, 2, 3, 4, 5]
    >>> ivan
    [1, 2, 3, 4, 5]
    
    >>> foo()
    [5, 5, 5]
    
    singleton = None
    
    def use_singleton():
        global singleton
    
        if singleton is None:
            singleton = _make_singleton()
    
        return singleton.use_me()
    
    # _make_singleton() is called only once when the def is executed
    def use_singleton(singleton=_make_singleton()):
        return singleton.use_me()
    
    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
    
    def notastonishinganymore(a = []): 
        '''The name is just a joke :)'''
        a = a[:]
        a.append(5)
        return a
    
    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(a)
    5347866528
    
    >>> id(b)
    5347866528
    
    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]
    
    >>> def func(a = []):
    ...    a.append(5)
    
    >>> def func(a = []):
    ...     a.append(5)
    ...     
    
    >>> func.__defaults__
    ([],)
    
    >>> func()
    
    >>> func.__defaults__
    ([5],)
    
    >>> 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
        ...