问题背景:
在Python中,参数传递实现了"按对象引用调用"的原则(有时称为按共享调用)。这意味着函数内部的变量开始指向与外部传递的参数相同的内存对象。
问题:
如果函数修改了传入的可变对象(例如列表或字典),变化在函数外部也是可见的。这可能导致难以捕捉的错误,特别是当期望函数不会修改输入数据时。
解决方案:
为了避免副作用,应在函数内部创建对象的副本或使用不可变数据结构。复制时可以使用标准方法(例如,列表的list.copy()、字典的dict.copy()或copy.deepcopy())。
代码示例:
def append_one(xs): xs.append(1) return xs lst = [0] append_one(lst) print(lst) # [0, 1] # 如何避免更改?创建副本: def safe_append_one(xs): ys = xs.copy() ys.append(1) return ys lst2 = [0] safe_append_one(lst2) print(lst2) # [0]
关键特性:
通过.copy()复制的列表副本可以完全独立于原始列表吗?
不可以 — .copy() 创建的是浅拷贝。如果内部有嵌套的可变对象,变更会在原件中显示。
import copy lst = [[1, 2], [3, 4]] shallow = lst.copy() shallow[0][0] = 42 print(lst) # [[42, 2], [3, 4]] deep = copy.deepcopy(lst) deep[0][0] = 100 print(lst) # [[42, 2], [3, 4]]
基于输入返回新对象是否保证原件不被更改?
不一定。如果新对象内部使用原始对象的部分(例如,指向内部嵌套列表的引用),原始对象可能会被更改。
def duplicate_list(xs): return xs * 2 lst = [[1], [2]] res = duplicate_list(lst) res[0][0] = 999 print(lst) # [[999], [2]]
默认可变对象的参数在多次调用函数时会导致问题吗?
是的 — 默认值只在函数定义时计算一次。
def add_item(item, container=[]): container.append(item) return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2]
负面案例
在配置处理库中使用列表作为默认值,这导致在不同函数调用之间累积元素。行为不可预测,且检测时间较长。
优点:
较少的代码用于重复调用,明显的内存节省。
缺点:
隐式行为,调试困难,长期错误。
正面案例
使用None作为默认值,并在每次调用时显式创建新对象。
优点:
可预测性,没有意外的副作用,可靠性。
缺点:
需要意识到并多写一点代码。