返回

Python 初始化方法默认列表引用陷阱及解决

python

Python 初始化方法中默认列表为何是引用

在Python类初始化方法 (__init__) 中使用可变对象(比如列表)作为默认参数时,会遇到一个常见的陷阱:默认参数在所有实例之间共享。这种行为可能导致意想不到的结果,正如题目中示例代码所展示的那样。理解这种现象的原因至关重要,它关乎编写健壮、可预测的代码。

默认参数的绑定时机

Python中,函数的默认参数值在函数定义时就被计算一次。这个计算时机发生在代码解析阶段,而不是函数每次被调用的时候。 对于不可变类型(如整数、字符串、元组),这种机制不会产生问题,因为它们是值传递,每次调用都会得到新的值。 但当默认参数是可变类型(如列表、字典)时,函数将维护对同一对象的引用。这意味着,多个实例在没有显式修改实例属性的情况下,默认参数所指向的内存位置其实是相同的。这就是示例中 banana_1.price_history 的修改会影响到 banana_2.price_history 的原因:它们都引用着在 banana 类定义时就创建的同一个空列表。

解决方式

要避免这个问题,通常需要避免使用可变对象作为默认参数,而是使用 None 作为默认值。然后在 __init__ 方法内部,检测参数是否为 None。如果为 None,则初始化为所需的默认值。这样确保每次创建新的实例,都会获得一个新的可变对象,不会有共享引用。以下提供了两种常见的解决方法。

1. 使用 None 作为默认值并进行初始化

这种方法通过检查是否传入了初始值来创建一个新的列表。

class banana:
    def __init__(self, weight, price_history=None):
        self.weight = weight
        if price_history is None:
            self.price_history = []
        else:
           self.price_history = price_history

banana_1 = banana(1)
banana_2 = banana(2)

banana_1.price_history.append(1)
print(banana_1.price_history)  # 输出 [1]
print(banana_2.price_history)  # 输出 []

这种方法保证了每个实例拥有各自的 price_history 列表,banana_1 对其列表的更改不会影响到 banana_2 的列表。

步骤说明:

  1. 初始化 __init__ 方法时,使用 None 作为默认值,而不是空列表 []
  2. __init__ 方法中添加条件判断,判断传入的 price_history 是否为 None
  3. 如果为 None, 则初始化实例的 price_history 属性为一个新的空列表。
  4. 如果不是 None (有传入 price_history),则使用传入的参数。
  5. 实例化 banana_1banana_2
  6. banana_1.price_history 添加元素。
  7. 输出结果 banana_1price_history[1], 而 banana_2price_history[]

2. 使用可变对象的副本作为默认值 (不推荐)

可以利用 copy() 方法或 list() 等函数为可变对象创建副本,避免共享引用。但是,这样做会让代码看起来复杂,不直观,同时可变对象深浅复制也容易引入额外的麻烦。 不推荐使用。 举例:

class banana:
    def __init__(self, weight, price_history = list()):
        self.weight = weight
        self.price_history = price_history

banana_1 = banana(1)
banana_2 = banana(2)

banana_1.price_history.append(1)

print(banana_1.price_history)  
print(banana_2.price_history) 

这个例子表面上可以解决 可变默认参数的问题,但实际上并没有解决 ,因为每次都会创建一个全新的list()对象,等价于下面的形式。为了阐明这个问题,修改一下代码如下,这样更能说明问题所在:

class banana:
    default_history = []  #  class variable, defined on module loading 

    def __init__(self, weight, price_history=None):
        self.weight = weight
        if price_history is None:
            self.price_history = list(banana.default_history) #创建一个空列表,但是每次都共享了 default_history
        else:
           self.price_history = price_history

banana_1 = banana(1)
banana_2 = banana(2)
banana_1.price_history.append(1)


print(banana_1.price_history)
print(banana_2.price_history)


print(id(banana_1.price_history), id(banana_2.price_history), id(banana.default_history))

在实际开发中,不应假设类变量,尤其对于可能存储共享可变对象的类变量做修改,因为你很难理解共享对象发生了哪些变化。推荐使用上一个例子中“使用 None 作为默认值”的做法,并且配合合理的设计,在必要的情况下传递自定义初始值。

最佳实践建议

在实际项目开发中,使用 None 作为默认值,并配合逻辑判断和实例化赋值。 这样的处理不仅使代码更安全,还让程序易于阅读和维护。对于复杂的初始化场景,如果存在默认值来源的需求,应尽量使用模块或包级别的只读全局变量或函数生成器替代直接硬编码在类中,并及时考虑使用工厂模式封装实例化过程。始终将类设计为内聚性更强,避免在类中初始化不必要全局共享状态。 在调试此类问题时,可以使用 id() 函数来检查变量的内存地址,验证共享引用问题。

通过仔细选择参数默认值的初始化方式,可以写出更稳健、可维护的Python代码,避免潜在的bug。这应该是一个程序员需要熟悉掌握的基础技能。