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

2024-11-15 08:37:00
admin
原创
14
摘要:问题描述:任何长期使用 Python 的人都曾被下面的问题困扰过(或者说被折磨得遍体鳞伤):def foo(a=[]): a.append(5) return a Python 新手会认为这个不带参数的函数总是返回一个只有一个元素的列表:[5]。但结果却大不相同,而且非常令人惊讶(对于新手来说...

问题描述:

任何长期使用 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行将是“混合的”,因为绑定(函数对象)的一部分会在定义时发生,而部分(默认参数的分配)会在函数调用时发生。

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


解决方案 1:

事实上,这并不是设计缺陷,也不是因为内部结构或性能。这仅仅是因为 Python 中的函数是一等对象,而不仅仅是一段代码。

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

无论如何,effbot(Fredrik Lundh)在《Python 中的默认参数值》中对这种行为的原因进行了非常好的解释。我发现它非常清楚,我真的建议阅读它以更好地了解函数对象的工作原理。

解决方案 2:

假设你有以下代码

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

def eat(food=fruits):
    ...

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

但是,假设稍后我在代码中做了类似的事情

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

那么如果默认参数是在函数执行时而不是在函数声明时绑定的,我会惊讶地(非常糟糕地)发现水果已被更改。在我看来,这比发现foo上面的函数正在改变列表更令人惊讶。

真正的问题在于可变变量,所有语言都或多或少存在这个问题。这里有一个问题:假设在 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:

文档的相关部分:

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

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

解决方案 4:

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

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

a = []

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

为什么a=[]

def x(a=[]):

在函数定义时实例化一个新列表,而不是在调用时实例化?这就像你在问“如果用户不提供参数,则实例化一个新列表并使用它,就像它是由调用者生成的一样”。我认为这反而含糊不清:

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

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

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

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

def x(static a=b):

解决方案 5:

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

比较一下:

class BananaBunch:
    bananas = []

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

这段代码也遭遇了完全相同的意外情况。bananas 是一个类属性,因此,当你向其添加内容时,它会被添加到该类的所有实例中。原因完全相同。

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

是的,这是意料之外的。但一旦明白过来,它就与 Python 的一般工作方式完美契合。事实上,它是一种很好的教学辅助工具,一旦你理解了为什么会发生这种情况,你就会更好地理解 Python。

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

解决方案 6:

为何不反省一下呢?

真的很惊讶没有人对可调用函数执行过 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__函数对象的属性中(相关部分:可调用函数):

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

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

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=[]

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

解决方案 8:

捍卫 Python 的 5 点建议

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

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

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

  4. 充足的文档:在最基本的 Python 文档(教程)中,这个问题在“更多关于定义函数”一节的
    第一小节中被大声宣布为“重要警告”。警告甚至使用了粗体,这在标题之外很少使用。RTFM:阅读精美手册。

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

解决方案 9:

这种行为很容易解释:

  1. 函数(类等)声明只执行一次,创建所有默认值对象

  2. 一切都通过引用传递

所以:

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

解决方案 10:

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

类似 SO 问题的示例问题

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)在某些情况下,默认参数的可变行为是有用的。

解决方案 11:

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

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

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

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

解决方案 12:

这实际上与默认值无关,只是当您编写具有可变默认值的函数时,它常常会出现意外行为。

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

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

解决方案 13:

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

这是否违反了“最小惊讶”原则?

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

解决方案 14:

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

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

解决方案 15:

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

def a(): return []

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

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

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

解决方案 16:

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

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

解决方案 17:

如果考虑到以下情况,这种行为就不足为奇了:

  1. 只读类属性在分配尝试时的行为,以及

  2. 函数是对象(在接受的答案中解释得很好)。

(2)的作用已在本主题中进行了广泛的讨论。(1)可能是造成惊讶的因素,因为当来自其他语言时,这种行为并不“直观”。

(1)在 Python 类教程中有描述。尝试为只读类属性分配一个值:

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

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

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

这里foo是一个对象,a是的属性foo(可在 处获得foo.func_defs[0])。由于a是一个列表,a所以是可变的,因此是 的读写属性foo。它在函数实例化时按照签名指定的方式初始化为空列表,只要函数对象存在,就可以读写。

调用foo而不覆盖默认值将使用 中的默认值foo.func_defs。在这种情况下,foo.func_defs[0]用于a函数对象代码范围内。更改为achange 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,并用于函数调用的其余部分。foo.func_defs[0]因此,对于将来的调用保持不变foo

解决方案 18:

事实可能是如此:

  1. 有人正在使用每种语言/库功能,并且

  2. 在这里改变行为是不明智的,但是

坚持上述两个特点是完全一致的,并且还提出了另一点:

  1. 这是一个令人困惑的特性,并且是 Python 中的不幸之举。

其他答案,或者至少其中的一些答案,要么提到了第 1 点和第 2 点但没有提到第 3 点,要么提到了第 3 点而淡化了第 1 点和第 2 点。但这三个答案都是正确的。

也许中途换马会导致严重的破坏,而将 Python 改为直观地处理 Stefano 的开头片段可能会带来更多问题。也许熟悉 Python 内部原理的人可以解释一系列后果。然而,

现有的行为不符合 Python 风格,而 Python 之所以成功,是因为该语言很少严重违反最小惊讶原则。无论是否应该将其根除,这都是一个真正的问题。这是一个设计缺陷。如果您通过尝试追踪行为来更好地理解该语言,我可以说 C++ 可以做到这一切,甚至更多;例如,您可以通过导航细微的指针错误来学习很多东西。但这不是 Python 风格:那些足够关心 Python 并坚持面对这种行为的人,是因为 Python 比其他语言的意外少得多,所以被该语言吸引。业余爱好者和好奇者成为 Pythonistas 是因为他们对让某件事工作所需的时间之短感到惊讶——不是因为设计缺陷——我的意思是隐藏的逻辑谜题——这与那些被 Python 吸引的程序员的直觉相悖,因为他们被 Python 吸引是因为它就是有效

解决方案 19:

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

解决方案 20:

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

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

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

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

根据 Brett Slatkin 的《有效的 Python:编写更好的 Python 的 59 种具体方法》,第 20 项:使用None和文档字符串指定动态默认参数(第 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_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]

对于“错误方法”可能存在合法的用例,即程序员想要共享默认列表参数,但这更可能是例外而不是规则。

解决方案 21:

是的,这是 Python 的一个设计缺陷

我读了所有其他答案,但我并不信服。这种设计确实违反了最小惊讶原则。

可以将默认值设计为在调用函数时进行评估,而不是在定义函数时进行评估。JavaScript 就是这样做的:

function foo(a=[]) {
  a.push(5);
  return a;
}
console.log(foo()); // [5]
console.log(foo()); // [5]
console.log(foo()); // [5]

运行代码片段Hide results展开片段

作为这是设计缺陷的进一步证据,Python 核心开发人员目前正在讨论引入新语法来修复此问题。请参阅本文:Python 的后期绑定参数默认值。

为进一步证明这是一个设计缺陷,如果你在 Google 上搜索“Python 陷阱”,这个设计会被列为一个陷阱,通常是列表中的第一个陷阱,在前 9 个 Google 搜索结果中(1、2、3、4、5、6、7、8、9)。相比之下,如果你在 Google 上搜索“Javascript 陷阱”,Javascript 中默认参数的行为甚至一次都没有被列为陷阱。

根据定义,陷阱违反了最小惊讶原则。它们令人震惊。鉴于默认参数值的行为有更优的设计,不可避免的结论是,Python 的行为在这里代表了一个设计缺陷。

我是 Python 的爱好者,所以我这样说。我们可以是 Python 的粉丝,但仍然承认,每个对 Python 的这个方面感到不快的人之所以感到不快,是因为这一个真正的“陷阱”。

解决方案 22:

这里的解决方案是:

  1. 用作None默认值(或 nonce object),并在运行时启用它来创建你的值;或者

  2. 使用 alambda作为默认参数,并在 try 块内调用它以获取默认值(这是 lambda 抽象的用途)。

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

解决方案 23:

您可以通过替换对象(以及与范围的联系)来解决这个问题:

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

虽然丑陋,但确实有效。

解决方案 24:

当我们这样做时:

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]

解决方案 25:

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

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

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

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

解决方案 26:

其他所有答案都解释了为什么这实际上是一种很好的、​​理想的行为,或者为什么你根本不需要这样做。我的观点是针对那些固执的人,他们想行使自己的权利,让语言屈从于他们的意愿,而不是反过来。

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

import inspect
from copy import deepcopy  # copy would fail on deep arguments like nested dicts

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(deepcopy(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!

值得注意的是,如果您尝试使用关键字参数,上述解决方案将会中断,如下所示:

foo(a=[4])

可以调整装饰器以实现这一点,但我们将其留给读者作为练习;)

解决方案 27:

这个“错误”让我加班很多时间!但我开始看到它的潜在用途(但我仍然希望它在执行时出现)

我将给你一个我认为有用的例子。

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

打印以下内容

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

解决方案 28:

这不是设计缺陷。任何人遇到这种情况都是做错事了。

我发现有三种情况你可能会遇到这个问题:

  1. 您打算将参数修改为函数的副作用。在这种情况下,使用默认参数是没有意义的cache={}。唯一的例外是当您滥用参数列表来获得函数属性时,例如,并且您根本不需要使用实际参数来调用该函数。

  2. 您本想保留参数不变,但无意中却修改它。这是一个错误,请修复它。

  3. 您打算修改参数以在函数内部使用,但不希望修改在函数外部可见。在这种情况下,您需要复制参数,无论它是否是默认值!Python 不是按值调用语言,因此它不会为您复制,您需要明确说明。

问题中的例子可能属于第 1 类或第 3 类。奇怪的是它既修改了传递的列表又返回了它;您应该选择其中一个。

解决方案 29:

只需将函数改为:

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

解决方案 30:

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 time
    parameter = default if parameter is None else parameter
    ...
相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用