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

2024-12-02 08:41:00
admin
原创
164
摘要:问题描述:有时,使用一个空列表作为默认参数似乎很自然。然而,Python 在这些情况下会产生意想不到的行为。例如,考虑这个函数:def my_func(working_list=[]): working_list.append("a") print(working_list...

问题描述:

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

例如,考虑这个函数:

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

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

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


解决方案 1:

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”之间有什么区别

解决方案 2:

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

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

解决方案 3:

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

if working_list is None: working_list = []

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

working_list = working_list or []

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

解决方案 4:

您的问题重点是如何修复“在没有明确参数的情况下重复调用”时的行为。您还应该问自己,在使用参数

调用时希望发生什么:

>>> CONSTANT = ['the']
>>> my_func(CONSTANT)
['the', 'a']
>>> CONSTANT  # oops??
['the', 'a']

如果需要,请参阅 HenryR 的回答 ( =None,查看is None内部)。

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

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

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

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

  • 如果您按照 C 的“输出参数”习惯来做这件事,那就完全没有必要了——您总是可以将多个值作为元组返回。

  • 如果您这样做是为了高效地构建一长串结果列表而无需构建中间列表,请考虑将其编写为生成器并result_list.extend(myFunc())在调用时使用。这样您的调用约定就会保持非常清晰。

如果你觉得强制函数体不应该变异,可以选择以下防御步骤:

  • 使用不可变默认值:starting_list=()。有时需要更改代码,但复制也list()接受元组,例如return [*starting_list, "a"]

  • 添加类型注释并运行类型检查器。

PS经常对可选参数进行变异的一种模式是递归函数中隐藏的“备忘录”参数:

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)

解决方案 5:

回顾

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值。

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

这也意味着实例的方法不能使用实例的属性作为默认值:在确定默认值时,self不在范围内,并且实例不存在

>>> 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哨兵值

标准的、普遍被认为是惯用的方法是使用None默认值,并明确检查该值并在函数的逻辑中替换它。因此:

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

简单地避免可变的默认值

如果不需要为了实现函数的逻辑而修改参数,那么由于命令-查询分离的原则,最好不要这样做

根据这个论点,append_one_and_return一开始就设计得很糟糕:由于目的是显示输入的某些修改版本,它实际上不应该修改调用者的变量,而应该只创建一个用于显示目的的新对象。这允许使用不可变对象(例如元组)作为默认值。因此:

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 的支持。

解决方案 6:

我可能有点跑题了,但请记住,如果你只想传递可变数量的参数,那么 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}

解决方案 7:

引用自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

解决方案 8:

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

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

解决方案 9:

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

    def my_funct(params, lst = []):
        liste = lst.copy()
         . . 

解决方案 10:

我参加了 UCSC 的进修课程Python for programmer

以下哪项是正确的:def Fn(data = []):

a) 是一个好主意,这样您的数据列表在每次调用时都是空的。

b) 是一个好主意,这样所有对该函数的调用,如果在调用时不提供任何参数,就会得到空列表作为数据。

c) 只要您的数据是字符串列表,就是一个合理的想法。

d) 是个坏主意,因为默认的 [] 会累积数据,并且默认的 [] 会随着后续调用而改变。

回答:

d) 是个坏主意,因为默认的 [] 会累积数据,并且默认的 [] 会随着后续调用而改变。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

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

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

内置subversion和git源码管理

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

免费试用