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

如何避免由 Python 早期绑定的默认参数引起的问题(例如,可变默认参数“记住”旧数据)?

B''H Bi'ezras -- Boruch Hashem 2月前

108 0

有时,使用一个空列表作为默认参数似乎很自然。然而,Python 在这些情况下会产生意想不到的行为。例如,考虑这个函数:def my_func(

有时,使用一个空列表作为默认参数似乎很自然。然而, Python 在这些情况下会产生意想不到的行为。 .

例如,考虑这个函数:

def my_func(working_list=[]):
    working_list.append("a")
    print(working_list)

第一次调用时,默认设置会起作用,但之后的调用将更新现有列表( "a" 每次调用一个)并打印更新后的版本。

我如何修复该函数,以便在没有明确参数的情况下重复调用它时,每次都使用一个新的空列表?

帖子版权声明 1、本帖标题:如何避免由 Python 早期绑定的默认参数引起的问题(例如,可变默认参数“记住”旧数据)?
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由B''H Bi'ezras -- Boruch Hashem在本站《object》版块原创发布, 转载请注明出处!
最新回复 (0)
  • kyl 2月前 0 只看Ta
    引用 2

    这实际上重复了 Beni Cherniavsky-Paskin 的答案,但细节却少得多。

  • 也许最简单的方法就是在脚本中创建列表或元组的副本。这样就无需检查。例如,

        def my_funct(params, lst = []):
            liste = lst.copy()
             . . 
    
  • 已经提供了好的和正确的答案。我只是想给出另一种语法来写你想要做的事情,当你想创建一个带有默认空列表的类时,我发现这种语法更漂亮:

    class Node(object):
        def __init__(self, _id, val, parents=None, children=None):
            self.id = _id
            self.val = val
            self.parents = parents if parents is not None else []
            self.children = children if children is not None else []
    

    此代码片段使用了 if else 运算符语法。我特别喜欢它,因为它是一行简洁的语句,没有冒号等,读起来几乎像一个正常的英语句子。:)

    在你的情况下你可以写

    def myFunc(working_list=None):
        working_list = [] if working_list is None else working_list
        working_list.append("a")
        print working_list
    
  • 引用自 https://docs.python.org/3/reference/compound_stmts.html#function-definitions

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

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

    Python 会提前评估参数/参数的默认值 ;它们是“早期绑定的”。这可能会以几种不同的方式导致问题。例如:

    >>> import datetime, time
    >>> def what_time_is_it(dt=datetime.datetime.now()): # chosen ahead of time!
    ...     return f'It is now {dt.strftime("%H:%M:%S")}.'
    ... 
    >>> 
    >>> first = what_time_is_it()
    >>> time.sleep(10) # Even if time elapses...
    >>> what_time_is_it() == first # the reported time is the same!
    True
    

    然而,问题最常见的表现方式是当函数的参数是 可变的 (例如, a list )时,并在函数的代码中发生变异。 当发生这种情况时,更改将被“记住” ,从而在后续调用中“看到”:

    >>> def append_one_and_return(a_list=[]):
    ...     a_list.append(1)
    ...     return a_list
    ... 
    >>> 
    >>> append_one_and_return()
    [1]
    >>> append_one_and_return()
    [1, 1]
    >>> append_one_and_return()
    [1, 1, 1]
    

    因为 a_list 是提前创建的,所以每次调用使用默认值的函数时都将使用 相同的列表对象 ,该对象在每次调用时都会被修改,并附加另一个 1 值。

    这是一个 有意识的设计决策 可以在某些情况下加以利用 - 尽管通常有更好的方法来解决其他问题。(考虑 using functools.cache or functools.lru_cache 进行记忆,并 functools.partial 绑定函数参数。)

    这也意味着 实例的方法不能使用实例的属性作为默认值 :在确定默认值时, self is not in scope, and the instance does not exist anyway

    >>> class Example:
    ...     def function(self, arg=self):
    ...         pass
    ... 
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 2, in Example
    NameError: name 'self' is not defined
    

    (该类 Example 还不 存在,并且名称 Example 也不范围内;因此, 即使我们不关心可变性问题,类属性在这里

    解决方案

    用作 None 标记值

    标准的、普遍被认为是惯用的方法是 use None as the default value, and explicitly check 该值并在函数的逻辑中替换它。因此:

    >>> def append_one_and_return_fixed(a_list=None):
    ...     if a_list is None:
    ...         a_list = []
    ...     a_list.append(1)
    ...     return a_list
    ... 
    >>> append_one_and_return_fixed([2]) # it works consistently with an argument
    [2, 1]
    >>> append_one_and_return_fixed([2])
    [2, 1]
    >>> append_one_and_return_fixed() # and also without an argument
    [1]
    >>> append_one_and_return_fixed()
    [1]
    

    这种 有效,是因为 代码 a_list = [] 在调用函数时 运行(如果需要) ,而不是提前运行——因此,它每次都会创建一个新的空列表。因此,这种方法也可以解决这个 datetime.now() 问题。这确实意味着函数不能将 None 值用于 其他 目的;然而,这不应该在普通代码中造成问题。

    简单地避免可变的默认值

    命令查询分离 的原则,不需要修改参数来实现函数的逻辑, 那么最好 不要这样做 .

    By this argument, append_one_and_return is poorly designed to begin with :由于其目的是 显示 输入的某些修改版本,因此它实际上不应该 修改 调用者的变量,而应该只创建一个 新对象 。这允许使用不可变对象(例如元组)作为默认值。因此:

    def with_appended_one(a_sequence=()):
        return [*a_sequence, 1]
    

    这样,即使明确提供了输入,也可以避免修改输入:

    >>> x = [1]
    >>> with_appended_one(x)
    [1, 1]
    >>> x # not modified!
    [1]
    

    它无需参数就可以正常工作,即使反复使用也是如此:

    >>> with_appended_one()
    [1]
    >>> with_appended_one()
    [1]
    

    并且它还获得了一些灵活性:

    >>> with_appended_one('example') # a string is a sequence of its characters.
    ['e', 'x', 'a', 'm', 'p', 'l', 'e', 1]
    

    PEP 671

    PEP 671 提议为 Python 引入新语法,允许显式后期绑定参数的默认值。建议的语法是:

    def append_and_show_future(a_list=>None): # note => instead of =
        a_list.append(1)
        print(a_list)
    

    然而,虽然该 PEP 草案提议在 Python 3.12 中引入该功能,但这并没有实现,而且 目前还没有这样的语法可用 最近关于这个想法的讨论,但它似乎不太可能在不久的将来得到 Python 的支持。

  • 我可能有点跑题了,但请记住,如果你只想传递可变数量的参数,那么 Python 风格的方法是传递一个元组 *args 或一个字典 **kargs 。这些都是可选的,比语法更好 myFunc([1, 2, 3]) .

    如果你想传递一个元组:

    def myFunc(arg1, *args):
      print args
      w = []
      w += args
      print w
    >>>myFunc(1, 2, 3, 4, 5, 6, 7)
    (2, 3, 4, 5, 6, 7)
    [2, 3, 4, 5, 6, 7]
    

    如果你想传递一本字典:

    def myFunc(arg1, **kargs):
       print kargs
    >>>myFunc(1, option1=2, option2=3)
    {'option2' : 2, 'option1' : 3}
    
  • 如果参数不会改变,那么使用不可变对象作为默认值(此处为 ())也是有意义的。根据上下文,这可能需要对代码进行轻微更改(但此处不需要;list(()) 可以很好地创建新的空列表)。

  • 如果函数的目的是 修改 传递的参数 working_list ,请参见 HenryR 的回答(= None,检查里面是否为 None )。

    但是如果你不打算改变参数,只是将其用作列表的起点,那么你可以简单地复制它:

    def myFunc(starting_list = []):
        starting_list = list(starting_list)
        starting_list.append("a")
        print starting_list
    

    (或者在这个简单的情况下, print starting_list + ["a"] 但我猜这只是一个玩具例子)

    一般来说,在 Python 中改变参数是一种不好的做法。唯一可以完全改变对象的函数是对象的方法。改变可选参数的情况就更少见了——只在某些调用中发生的副作用真的是最好的接口吗?

    • p4

    • p5

    对可选参数进行变异的一种模式递归函数中隐藏的“memo”参数:

    def depth_first_walk_graph(graph, node, _visited=None):
        if _visited is None:
            _visited = set()  # create memo once in top-level call
    
        if node in _visited:
            return
        _visited.add(node)
        for neighbour in graph[node]:
            depth_first_walk_graph(graph, neighbour, _visited)
    
  • 或建议看起来不错,但当提供 0 与 1 或 True 与 False 时,其行为会令人惊讶。

  • 在这种情况下这并不重要,但您可以使用对象标识来测试 None :

    if working_list is None: working_list = []
    

    您还可以利用布尔运算符或 Python 中的定义方式:

    working_list = working_list or []
    

    但是,如果调用者给你一个空列表(算作假)作为 working_list,并期望你的函数修改他给出的列表,则会出现意外行为。

  • 其他答案已经提供了所要求的直接解决方案,但是,由于这是新 Python 程序员经常犯的一个错误,因此值得补充一下 Python 为何如此表现的解释,这在《 Python 漫游指南》的可变 默认参数:

    Python 的默认参数在函数定义时被求值 一次 ,而不是每次调用函数时都求值(就像 Ruby 中那样)。这意味着,如果您使用可变的默认参数并对其进行变异,那么您在将来对该函数的所有调用中对该对象进行变异。

  • @myke 我刚刚编辑了一些相关信息。查看链接了解详情。working_list 或具有与 working_list 相同的考虑。

  • 如果 working_list 为 None,那么 working_list = working_list 或 [] 而不是 working_list = [] 怎么样?否则 working_list?

  • 这个习惯用法在两种情况下很有用:当你改变一个参数时,以及当你想在调用函数时确定默认值时。后者更为重要。

  • def my_func(working_list=None):
        if working_list is None: 
            working_list = []
    
        # alternative:
        # working_list = [] if working_list is None else working_list
    
        working_list.append("a")
        print(working_list)
    

    文档 说你应该使用它 None 作为默认值并在函数主体中明确测试它。

    除此以外

    x is None PEP 8 推荐的 比较 :

    与 None 等单例的比较应该始终使用 is is not ,而永远不要使用相等运算符。

    当你真正想写 if x ,要小心。 if x is not None [...]

    另请参阅 \'is None\' 和 \'== None\' 之间有什么区别

  • - 不,函数定义是在执行函数定义时定义的,至少在 CPython 中是这样。您使用的是 JIT 编译器还是其他东西?

  • 由于链接失效,让我明确指出这是期望的行为。默认变量在函数定义时进行评估(第一次调用时发生),而不是每次调用函数时都进行评估。因此,如果您改变可变默认参数,则任何后续函数调用都只能使用改变的对象。

  • 同样的行为也发生在集合上,不过你需要一个稍微复杂一点的例子才能将它显示为错误。

返回
作者最近主题: