8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

“最小惊讶”和可变默认参数

BBB 2月前

179 0

任何对 Python 进行过长期修改的人都会被下面的问题所困扰(或撕碎):def foo(a=[]): a.append(5) return aPython 新手会期望这个函数被调用...

任何长期使用 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、本帖标题:“最小惊讶”和可变默认参数
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由BBB在本站《class》版块原创发布, 转载请注明出处!
最新回复 (0)
  • 我毫不怀疑可变参数违反了普通人的最小惊讶原则,我见过初学者踏入这一领域,然后英勇地用邮件元组替换邮件列表。然而,可变参数仍然符合 Python Zen (Pep 20) 并属于“荷兰人显而易见”条款(被铁杆 Python 程序员理解/利用)。推荐的解决方法是使用文档字符串,这是最好的,但如今抵制文档字符串和任何(书面)文档的情况并不罕见。就我个人而言,我更喜欢装饰器(比如 @fixed_defaults)。

  • 当我遇到这种情况时,我的论点是:“为什么你需要创建一个返回可变变量的函数,而这个可变变量可以是传递给函数的可变变量?它要么改变可变变量,要么创建一个新的可变变量。为什么你需要用一个函数做这两件事?为什么要重写解释器,让你在不添加三行代码的情况下做到这一点?”因为我们在这里讨论的是重写解释器处理函数定义和调用的方式。对于一个几乎没有必要的用例来说,这需要做很多事情。

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

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

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

  • 对于阅读上述答案的任何人,我强烈建议您花时间阅读链接的 Effbot 文章。除了所有其他有用的信息外,有关此语言功能如何用于结果缓存/记忆的部分也非常有用!

  • 即使它是一等对象,人们仍可以设想一种设计,其中每个默认值的代码与对象一起存储,并在每次调用函数时重新评估。我并不是说这会更好,只是函数是一等对象并不能完全排除它。

  • 抱歉,但任何被认为是“Python 中最大的 WTF”的东西肯定都是设计缺陷。这在某个时候会给每个人带来错误,因为一开始没有人会想到这种行为 - 这意味着它不应该以这种方式设计。我不在乎他们必须经历什么困难,他们应该将 Python 设计为默认参数是非静态的。

  • 无论这是否是设计缺陷,您的回答似乎都暗示这种行为在某种程度上是必要的、自然的和显而易见的,因为函数是一等对象,但事实并非如此。Python 有闭包。如果您在函数的第一行用赋值替换默认参数,它会在每次调用时评估表达式(可能使用在封闭范围内声明的名称)。完全没有理由说每次以完全相同的方式调用函数时都评估默认参数是不可能或不合理的。

  • 设计并不直接遵循函数是对象这一原则。在您的范例中,建议将函数的默认值实现为属性而不是特性。

  • 假设你有以下代码

    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 当前的做法:在定义函数时评估默认函数参数,并且该对象始终是默认参数。我想他们可以使用空列表进行特殊处理,但这种特殊处理会引起更大的惊讶,更不用说向后不兼容了。

  • 引用 11

    我认为这是一个有争议的问题。您正在对全局变量采取行动。现在,代码中任何地方执行的涉及全局变量的任何评估都将(正确地)引用(\'blueberries\',\'mangos\')。默认参数可能与其他任何情况一样。

  • 实际上,我不认为我同意你的第一个例子。我不确定我是否喜欢这样修改初始化器的想法,但如果我喜欢,我希望它的行为完全符合你的描述——将默认值更改为 (\'blueberries\', \'mangos\')。

  • 引用 13

    默认参数与其他情况一样。出乎意料的是,该参数是全局变量,而不是本地变量。这又是因为代码是在函数定义时执行的,而不是在调用时执行的。一旦你明白了这一点,并且类也是如此,那就很清楚了。

  • 我觉得这个例子很误导人,而不是很精彩。如果 some_random_function() 附加到水果而不是赋值给它,eat() 的行为就会改变。目前的设计太棒了。如果你使用在其他地方引用的默认参数,然后从函数外部修改引用,你就是在自找麻烦。真正的 WTF 是当人们定义一个新的默认参数(列表文字或对构造函数的调用)时,仍然会受到攻击。

  • 引用 15

    您刚刚明确声明了全局并重新分配了元组 - 如果之后 eat 的工作方式有所不同,那就不足为奇了。

  • Zeus 2月前 0 只看Ta
    引用 16

    文档 的相关部分 :

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

    def whats_on_the_telly(penguin=None):
        if penguin is None:
            penguin = []
        penguin.append("property of the zoo")
        return penguin
    
  • @bukzor:陷阱需要注意并记录下来,这就是为什么这个问题很好并且得到了这么多的赞同。同时,陷阱不一定需要被消除。有多少 Python 初学者将列表传递给修改它的函数,并惊讶地看到更改显示在原始变量中?然而,当您了解如何使用可变对象类型时,它们非常棒。我想这归结为对这个特定陷阱的看法。

  • 短语“这通常不是预期的”的意思是“这不是程序员真正希望发生的事情”,而不是“这不是 Python 应该做的事情”。

  • @holdenweb 哇,我来晚了。考虑到上下文,bukzor 完全正确:他们记录的行为/后果并非“预期”的,因为他们决定语言应该执行函数的定义。由于这是他们设计选择的非预期后果,所以这是一个设计缺陷。如果不是设计缺陷,就没有必要提供“解决这个问题的方法”。

  • 我们可以拿它来聊天,讨论它还能有其他什么样子,但语义已经被彻底争论过了,没有人能想出一个合理的机制来创建调用时的默认值。一个严重的问题是,调用时的范围通常与定义时的范围完全不同,如果在调用时评估默认值,则名称解析将不确定。“绕开”的意思是“您可以通过以下方式实现您想要的结果”,而不是“这是 Python 设计中的一个错误”。

返回
作者最近主题: