为什么“可变的默认参数修复”语法如此丑陋,请问python newbie

Why the “mutable default argument fix” syntax is so ugly, asks python newbie

下面是我的"python菜鸟问题"系列,基于另一个问题。

特权

转到http://python.net/~goodger/projects/pycon/2007/adilmatic/messate.html其他语言有变量,并向下滚动到"默认参数值"。您可以在这里找到以下内容:

1
2
3
4
5
6
7
8
9
def bad_append(new_item, a_list=[]):
    a_list.append(new_item)
    return a_list

def good_append(new_item, a_list=None):
    if a_list is None:
        a_list = []
    a_list.append(new_item)
    return a_list

在python.org上甚至有一个"重要警告",用同样的例子,但并不是说它"更好"。

一种说法

所以,这里的问题是:为什么在一个已知问题上"好"的语法难看,就像在一个提倡"优雅语法"和"易于使用"的编程语言中一样?

编辑:

换个说法

我不是在问为什么或者如何发生(感谢马克提供链接)。

我在问为什么没有更简单的内置语言替代方案。

我认为更好的方法可能是能够在def本身中做一些事情,其中name参数将附加到def中的"本地"或"新"可变对象。比如:

1
2
3
def better_append(new_item, a_list=immutable([])):
    a_list.append(new_item)
    return a_list

我相信有人能提供更好的语法,但我也猜这一定是一个很好的解释为什么没有这样做。


这被称为"可变默认陷阱"。参见:http://www.ferg.org/projects/python_gotchas.html contents_item_6

基本上,当程序第一次被解释时,不是每次调用函数时(正如您从其他语言所期望的那样),都会初始化a_list。因此,每次调用函数时都不会得到一个新的列表,但会重用同一个列表。

我想问题的答案是,如果你想在一个列表中附加一些东西,就这么做,不要创建一个函数来做。

这是:

1
2
>>> my_list = []
>>> my_list.append(1)

比以下内容更清晰易读:

1
>>> my_list = my_append(1)

在实际情况下,如果需要这种行为,您可能会创建自己的类,该类有方法来管理它的内部列表。


默认参数在执行def语句时进行评估,这可能是最合理的方法:它通常是需要的。如果不是这样,当环境发生一点变化时,它可能会导致混淆的结果。

用神奇的local方法或类似的方法进行区分,这是不理想的。python试图让事情变得非常简单,而当前的样板文件没有明显的、清晰的替代品,它没有诉诸于干扰python当前具有的相当一致的语义。


一个函数的极端特定的用例,它允许您有选择地传递一个要修改的列表,但是除非您特别传递一个列表,否则会生成一个新的列表,这绝对不值得使用特殊的用例语法。说真的,如果您要对这个函数进行多次调用,为什么要对序列中的第一个调用进行特殊说明(只传递一个参数),以便将其与其他调用区分开来(需要两个参数才能不断丰富现有列表)?!例如,考虑这样的事情(当然,假设betterappend做了一些有用的事情,因为在当前的例子中,将它称为直接.append会很疯狂!-)

1
2
3
4
5
6
7
8
9
10
def thecaller(n):
  if fee(0):
    newlist = betterappend(foo())
  else:
    newlist = betterappend(fie())
  for x in range(1, n):
    if fee(x):
      betterappend(foo(), newlist)
    else:
      betterappend(fie(), newlist)

这简直是疯了,显然应该是,

1
2
3
4
5
6
7
def thecaller(n):
  newlist = []
  for x in range(n):
    if fee(x):
      betterappend(foo(), newlist)
    else:
      betterappend(fie(), newlist)

总是使用两个参数,避免重复,构建更简单的逻辑。

引入特殊情况语法鼓励和支持特殊情况下的用例,而鼓励和支持这种非常特殊的用例真的没有多大意义——现有的、完全规则的语法对于用例极其罕见的好用途是很好的;-)。


我已经编辑了这个答案,以包含问题中发表的许多评论的想法。

你给出的例子有缺陷。它修改作为副作用传递给它的列表。如果您希望函数是这样工作的,那么使用默认参数是没有意义的。返回更新的列表也没有意义。如果没有默认参数,问题就会消失。

如果目的是返回一个新列表,则需要复制输入列表。python希望事情是明确的,所以由您来复制。

1
2
3
4
def better_append(new_item, a_list=[]):
    new_list = list(a_list)
    new_list.append(new_item)
    return new_list

对于稍有不同的内容,可以生成一个生成器,该生成器可以将列表或生成器作为输入:

1
2
3
4
def generator_append(new_item, a_list=[]):
    for x in a_list:
        yield x
    yield new_item

我认为您有一种误解,认为Python对可变和不变的默认参数的处理方式不同;这完全不是真的。相反,参数的不可变性使您以一种微妙的方式更改代码,以自动执行正确的操作。以您的示例为例,使其适用于字符串而不是列表:

1
2
3
def string_append(new_item, a_string=''):
    a_string = a_string + new_item
    return a_string

此代码不会更改传递的字符串-它不能更改,因为字符串是不可变的。它创建一个新字符串,并为该新字符串分配一个u字符串。默认参数可以反复使用,因为它不会改变,您在开始时复制了它。


如果你不是在谈论列表,而是关于awesomesets,一个你刚刚定义的类呢?您想在每个类中定义".local"吗?

1
2
3
4
class Foo(object):
    def get(self):
        return Foo()
    local = property(get)

可能有用,但很快就会变老。很快,"if a is none:a=correctObject()"模式就变成了第二天性,你不会发现它很难看——你会发现它很有启发性。

问题不是语法问题,而是语义问题——默认参数的值是在函数定义时计算的,而不是在函数执行时计算的。


也许您不应该将这两个函数定义为好函数和坏函数。您可以使用第一个列表或字典来实现对相应对象的就地修改。如果你不知道可变对象是如何工作的,但是你知道你在做什么,这个方法会让你头疼,在我看来这是可以的。

所以您有两种不同的方法来传递提供不同行为的参数。这很好,我不会改变的。


这比good_append()好,imo:

1
2
def ok_append(new_item, a_list=None):
    return a_list.append(new_item) if a_list else [ new_item ]

你也可以格外小心,检查一下清单是不是一个清单…


我认为你混淆了优雅的语法和句法的制糖。python语法清楚地传达了这两种方法,但与不正确的方法相比,正确的方法显得不那么优雅(就语法行而言)。但是,由于不正确的方法,是……不正确的,它的优雅是无关的。至于为什么像你在《更好的附加》中所展示的东西没有被实现,我想There should be one-- and preferably only one --obvious way to do it.在优雅方面胜过小收益。