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当前的方法:默认函数参数是在定义函数时计算的,并且该对象始终是默认的。我想它们可能是使用空列表的特殊情况,但这种特殊大小写会引起更大的惊讶,更不用说向后不兼容了。

假设您有以下代码

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新手会感到惊讶和被咬。但我认为,如果我们“修复”了这个问题,那么这只会造成一种不同的情况,他们反而会被咬,而这一点就更不直观了。此外,在处理
def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, 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()
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
    ...