DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Python 函数默认列表在多次调用之间不断累积元素

Python 可变默认参数在函数定义时创建,多次省略参数会共享同一个列表。用 None 在调用时创建列表,并明确显式传入列表时采用原地修改还是复制。

函数每次都省略列表参数,结果却越变越长,好像上一次调用的内容没有清空:

def collect(value, items=[]):
    items.append(value)
    return items

print(collect("first"))
print(collect("second"))
# 依次预期为 ['first']、['first', 'second']

Python 在执行函数定义时计算默认参数值;这里的列表在函数定义时创建一次,之后省略 items 的调用继续使用它。本文针对 Python 3 普通函数的可变默认参数。

append 修改的是同一个默认对象

默认表达式不会在每次调用时重新执行。append 原地修改列表,因此本次新增内容会成为下次调用的起点。字典、集合或其他可变对象作为默认值,也需要检查同样的问题。

返回值还可能与后续调用共享这个对象。例如先把第一次返回值保存下来,再调用一次函数,之前保存的列表也会出现新增元素。只比较每次函数返回瞬间的打印内容,容易漏掉这种别名关系。

用 None 表示没有传入列表

如果省略参数时应创建独立结果,把列表创建移到函数体内:

def collect(value, items=None):
    if items is None:
        items = []
    items.append(value)
    return items

first = collect("first")
second = collect("second")
print(first)           # 预期 ['first']
print(second)          # 预期 ['second']
print(first is second) # 预期 False

判断应写成 items is None,而不是 if not items。后者也会把调用方明确传入的空列表当作需要替换的值,可能破坏原来要求原地追加的行为。

这个版本保留了“显式传入列表就修改那个列表”的约定:

shared = []
result = collect("entry", shared)
print(shared)          # 预期 ['entry']
print(result is shared) # 预期 True

如果 None 本身有独立业务含义,不能直接把它当作缺省标记。那时应使用专用哨兵对象,并明确区分省略参数与显式传入 None;不要在没有检查调用方的情况下改变这一约定。

不允许修改传入列表时,另行复制

若函数应始终返回新列表,既不共享默认对象,也不修改调用方传入的容器,可以采用另一个版本:

def collect_copy(value, items=None):
    result = [] if items is None else list(items)
    result.append(value)
    return result

这里的输入约定是列表,返回新列表只是另一种接口语义,不能与上一个原地追加版本混用而不通知调用方。list(items) 复制容器中的元素引用;如果元素内部是可变对象,它们仍可能被共享。这不是深拷贝或隔离所有状态的方案。

不要简单在函数开头执行 items.clear()。它会清除同一个对象,之前持有返回值的代码也会受影响,明确传入的列表同样可能被清空。使用可变默认值再复制虽然能让当前函数不直接追加到默认对象,但容易继续保留共享默认状态;缺省值用 None 更便于确认创建时机。

修复后连续省略参数调用多次,检查结果独立;再分别传入空列表和已有元素的列表,确认调用方需要的是原地修改还是复制。还要核对第一次返回值在后续调用后是否保持不变。

已有进程中的旧函数对象与已经共享的返回值不会因为文件里改了一行而自动解除关联。按应用正常流程加载新函数定义,并检查之前的结果是否已被错误累积;回退时保留原接口约定,避免用清空共享对象的方式处理历史数据。