【编程基础】python 默认参数可变对象
self.state = WorkerState() 这个调用会触发 WorkerState 类的 __init__ 方法。
让我们来分解一下这个过程:
1. Python 中类的实例化过程
当你写下 ClassName(...) 这样的代码时,Python 解释器会执行以下两个主要步骤:
-
__new__(cls, *args, **kwargs):- 这是一个特殊的静态方法,它的主要工作是创建并返回一个类的空实例。
- 你通常不需要自己去重写
__new__,除非你在做一些非常高级的事情(比如创建单例模式,或者子类化不可变类型如int或str)。 - Python 的基类
object提供了默认的__new__实现。
-
__init__(self, *args, **kwargs):- 当
__new__创建并返回一个实例后,这个实例会作为第一个参数self传递给__init__方法。 __init__的作用不是“创建”对象,而是“初始化”这个已经创建好的空对象。它负责给这个对象设置初始属性值(比如self.step = -1)。
__init__方法没有返回值(或者说,它应该返回None)。
- 当
所以,self.state = WorkerState() 的完整流程是:
- 调用
WorkerState.__new__(WorkerState)来创建一个WorkerState的空对象。 - 然后,用这个新创建的空对象作为
self,调用WorkerState.__init__(self)。
2. WorkerState 类中的 __init__ 和 __post_init__
现在我们来看 WorkerState 类的具体定义:
import dataclasses
# ...
from dataclasses import dataclass
@dataclass
class WorkerState:
step: int = -1
log_history: List[Dict[str, float]] = None
kv: Dict[str, Union[float, Dict]] = None
def __post_init__(self):
if self.log_history is None:
self.log_history = []
if self.kv is None:
self.kv = {}
这里的关键是 @dataclass 装饰器。
@dataclass 是 Python 3.7+ 引入的一个非常有用的功能。当你用它来装饰一个类时,它会自动为你生成一些特殊方法,其中就包括 __init__。
@dataclass 做了什么?
它会读取你在类中定义的带有类型注解的属性(step: int, log_history: List[...] 等),并自动生成一个像下面这样的 __init__ 方法:
# 这是 @dataclass 自动生成的 __init__ 方法(示意代码)
def __init__(self, step: int = -1, log_history: List[Dict[str, float]] = None, kv: Dict[str, Union[float, Dict]] = None):
self.step = step
self.log_history = log_history
self.kv = kv
所以,当你调用 self.state = WorkerState() 时,实际上是在调用这个由 @dataclass 自动生成的 __init__ 方法。因为所有参数都有默认值,所以你可以不提供任何参数。此时:
step会被设置为默认值-1。log_history会被设置为默认值None。kv会被设置为默认值None。
3. __post_init__ 的作用
@dataclass 还提供了一个特殊的钩子方法:__post_init__。
这个方法会在 __init__ 方法执行完毕后立即被自动调用。
它的主要用途是处理一些需要在所有字段都被初始化之后才能进行的逻辑。在 WorkerState 中,它的作用是:
- 检查
self.log_history是否为None。如果是,就把它设置成一个空的列表[]。 - 检查
self.kv是否为None。如果是,就把它设置成一个空的字典{}。
为什么需要 __post_init__?
这是一个很好的编程实践,特别是为了避免一个常见的陷阱:在函数默认参数中使用可变类型(如列表或字典)。
如果你把 log_history 的默认值直接写成 []:
@dataclass
class BadWorkerState:
log_history: List[Dict[str, float]] = [] # 陷阱!
所有不提供 log_history 参数而创建的 BadWorkerState 实例将会共享同一个列表对象。修改一个实例的 log_history 会意外地影响到其他所有实例。
而 WorkerState 的设计通过 default=None 和 __post_init__ 完美地避开了这个问题。每个实例都会在 __post_init__ 中创建自己独立的空列表和空字典,确保了实例之间的状态隔离。
总结
self.state = WorkerState() 这个调用,依次触发了以下过程:
WorkerState.__new__(...): 创建一个WorkerState的空实例。- 由
@dataclass自动生成的__init__(...): 被调用,使用默认值初始化实例的step,log_history, 和kv属性(此时log_history和kv都是None)。 __post_init__(self): 在__init__之后被自动调用,它进一步处理实例,将log_history和kv从None变成各自独立的[]和{}。
所以,最终答案是:它调用了由 @dataclass 装饰器自动生成的 __init__ 方法,紧接着又调用了 __post_init__ 方法来完成最终的初始化。
不,None 不是一个可变对象。None 是一个不可变 (immutable) 的对象。
这是一个非常关键且常被误解的点,我们来详细梳理一下。
不可变对象 (Immutable Objects)
不可变对象指的是一旦创建,其内部状态(值)就永远不能被改变的对象。如果你试图“修改”它,实际上会创建一个新的对象。
常见的不可变类型包括:
- 数值类型:
int,float,complex - 字符串:
str - 元组:
tuple - 布尔值:
True,False None:NoneType类型的唯一实例
示例:
x = 10
print(id(x)) # 打印 x 的内存地址
x = x + 1 # 这不是在“修改”原来的 10,而是创建了一个新的值为 11 的对象
print(id(x)) # 内存地址会改变
s = "hello"
print(id(s))
s = s + " world" # 创建了一个新的字符串 "hello world"
print(id(s)) # 内存地址会改变
可变对象 (Mutable Objects)
可变对象指的是创建后,其内部状态(内容)可以被原地修改 (in-place modification),而不会创建新对象。
常见的可变类型包括:
- 列表:
list - 字典:
dict - 集合:
set - 自定义的类实例 (除非你特意将它们设计成不可变的)
示例:
my_list = [1, 2, 3]
print(id(my_list)) # 打印列表的内存地址
my_list.append(4) # 这是在“原地”修改列表,在其末尾添加一个元素
print(my_list) # 输出 [1, 2, 3, 4]
print(id(my_list)) # 内存地址不会改变!
my_dict = {"name": "Alice"}
print(id(my_dict))
my_dict["age"] = 30 # 原地修改字典
print(my_dict) # 输出 {'name': 'Alice', 'age': 30}
print(id(my_dict)) # 内存地址不会改变!
为什么默认参数使用 None 是安全的?
现在回到最初的问题,为什么 log_history: List[Dict[str, float]] = None 是安全的,而 log_history: List[Dict[str, float]] = [] 是危险的。
危险情况: default = []
def bad_function(items=[]):
items.append(1)
print(items)
bad_function() # 输出: [1]
bad_function() # 输出: [1, 1] <-- 意料之外的结果!
bad_function() # 输出: [1, 1, 1]
原因:函数定义在被解释器加载时,默认参数 [] 作为一个列表对象被创建一次。之后所有对 bad_function 的调用,如果没有提供 items 参数,都会共享这同一个列表对象。因此,对它的修改会累积下来。
安全情况: default = None
def good_function(items=None):
if items is None:
items = [] # 在函数体内创建新的列表
items.append(1)
print(items)
good_function() # 输出: [1]
good_function() # 输出: [1]
good_function() # 输出: [1]
原因:
- 默认参数
None是一个不可变对象。你不能“修改”None本身。 - 在函数
good_function的每次调用中,代码都会检查items是否是None。 - 如果是,
items = []这行代码会在每次调用时都创建一个全新的、独立的空列表对象。 - 因此,每次调用都是在操作一个崭新的列表,它们之间互不影响。
WorkerState 中的应用
在 WorkerState 的 __post_init__ 方法中,正是应用了这种安全的模式:
@dataclass
class WorkerState:
# 默认值是不可变的 None
log_history: List[Dict[str, float]] = None
kv: Dict[str, Union[float, Dict]] = None
def __post_init__(self):
# 在对象初始化时,为每个实例创建新的可变对象
if self.log_history is None:
self.log_history = []
if self.kv is None:
self.kv = {}
__init__(由@dataclass生成):s1 = WorkerState()->s1.log_history被设为None。s2 = WorkerState()->s2.log_history也被设为None。
__post_init__:- 对
s1调用__post_init__->s1.log_history变成一个新的空列表[](地址0x...A)。 - 对
s2调用__post_init__->s2.log_history变成另一个新的空列表[](地址0x...B)。
- 对
这样就确保了 s1.log_history 和 s2.log_history 是两个完全独立的对象,修改一个不会影响另一个。
总结:None 本身是不可变的,这使得它成为在函数或方法签名中充当可变类型默认值的完美“占位符”。真正的可变对象是在运行时(函数体或 __post_init__ 内部)为每个实例或每次调用动态创建的。
你提出了一个非常好的问题,这确实是 Python 中一个经典且容易让人困惑的“陷阱”。你说得对,s1 和 s2 这两个 BadWorkerState 实例本身确实不是同一个对象,它们有各自独立的内存地址。
然而,它们内部的 log_history 属性却指向了同一个列表对象。
让我们用一个生动的比喻和代码来彻底解释清楚这件事。
比喻:共享的“公共记事本”
想象一下,你开了一家公司,为每一位新员工 (BadWorkerState 实例) 分配工位。
- 员工
s1(id(s1)): 张三,工位号是A01。 - 员工
s2(id(s2)): 李四,工位号是B02。 - 张三和李四是两个不同的人,工位也不同。这对应了
s1和s2是两个不同的实例对象。
现在,你对公司的工作流程做了如下规定(这对应了 BadWorkerState 类的定义):
“所有员工,如果我没有特别为你指定私人记事本,你们就都去用挂在墙上的那个公共记事本(log_history 属性)来记录工作日志。”
这个“公共记事本”在公司成立(类被定义和加载)的那一刻,就已经被挂在墙上了,并且只有一本。
接下来发生了什么?
- 张三 (
s1) 入职:你告诉他:“你的记事本就是墙上那本。” 于是,张三的“记事本”指针就指向了那本公共记事本。 - 张三在记事本上写下了“完成了任务X”。
- 李四 (
s2) 入职:你也告诉他:“你的记事本就是墙上那本。” 于是,李四的“记事本”指针也指向了同一本公共记事本。 - 李四翻开记事本,他不仅能看到自己要写的内容,还能看到张三已经写下的“完成了任务X”。
- 李四接着写下了“完成了任务Y”。
- 现在,如果张三再去看记事本,他会发现上面有他自己写的“任务X”,还有李四写的“任务Y”。
结论:尽管张三和李四是两个独立的员工,但他们共享了同一个工作资源——那本独一无二的公共记事本。
代码层面解释
让我们用代码和内存地址 (id()) 来验证这个过程。
from dataclasses import dataclass, field
from typing import List, Dict
# 这是“危险”的设计
@dataclass
class BadWorkerState:
# 这个 [] 在类被定义时创建一次,并且只有一个
log_history: List[Dict[str, float]] = field(default_factory=list) # 错误的用法,等价于 []
# 更直接的错误演示
class BadClass:
def __init__(self, log_history=[]):
self.log_history = log_history
print("--- 演示 BadClass ---")
# 1. 创建两个不同的实例
s1 = BadClass()
s2 = BadClass()
# 2. 验证两个实例本身是不同的对象
print(f"s1 的内存地址: {id(s1)}")
print(f"s2 的内存地址: {id(s2)}")
print(f"s1 和 s2是同一个对象吗? {s1 is s2}") # 输出: False
print("-" * 20)
# 3. 验证它们内部的 log_history 属性是否指向同一个列表对象
print(f"s1.log_history 的内存地址: {id(s1.log_history)}")
print(f"s2.log_history 的内存地址: {id(s2.log_history)}")
print(f"s1.log_history 和 s2.log_history 是同一个对象吗? {s1.log_history is s2.log_history}") # 输出: True
print("-" * 20)
# 4. 修改其中一个实例的 log_history
s1.log_history.append({"step": 1, "loss": 0.5})
print(f"修改 s1 后,s1.log_history 是: {s1.log_history}")
print(f"修改 s1 后,s2.log_history 是: {s2.log_history}") # s2 的也跟着变了!
运行结果会是:
--- 演示 BadClass ---
s1 的内存地址: 140502123456784
s2 的内存地址: 140502123456896
s1 和 s2是同一个对象吗? False
--------------------
s1.log_history 的内存地址: 140502123500160
s2.log_history 的内存地址: 140502123500160
s1.log_history 和 s2.log_history 是同一个对象吗? True
--------------------
修改 s1 后,s1.log_history 是: [{'step': 1, 'loss': 0.5}]
修改 s1 后,s2.log_history 是: [{'step': 1, 'loss': 0.5}]
结果分析:
s1和s2的内存地址不同,证明它们是两个独立的实例对象。s1.log_history和s2.log_history的内存地址完全相同,证明这两个属性指向的是同一个列表对象。- 当我们对
s1.log_history进行.append()操作时,我们是在修改那个共享的列表对象。因此,当我们通过s2.log_history去查看时,看到的是同一个被修改过的列表。
为什么会这样?—— Python 的函数默认参数求值时机
这个行为的根源在于:Python 中,函数的默认参数值在函数被定义时只计算一次,而不是在函数被调用时计算。
对于 __init__(self, log_history=[]):
- 当 Python 解释器读取到
class BadClass:这段代码并定义这个类时,它会解析__init__方法。 - 它看到
log_history有一个默认参数[]。 - 于是,它在内存中创建了一个空列表对象,并把这个对象的引用作为
log_history的默认值。 - 这个过程只发生一次!
- 之后,每当你调用
BadClass()(不带参数),Python 都会把那个预先创建好的、唯一的列表对象的引用赋给新实例的self.log_history。
正确的做法 (default_factory)
dataclasses 模块意识到了这个问题,并提供了一个完美的解决方案:default_factory。
from dataclasses import dataclass, field
@dataclass
class GoodWorkerState:
# default_factory 接收一个可调用对象 (如 list 或 dict)
# 它会在每次创建新实例时被调用,从而生成新的对象
log_history: List[Dict[str, float]] = field(default_factory=list)
g1 = GoodWorkerState()
g2 = GoodWorkerState()
print(f"g1.log_history 和 g2.log_history 是同一个对象吗? {g1.log_history is g2.log_history}")
# 输出: False
default_factory=list 告诉 @dataclass:“不要在类定义时创建一个共享的列表。而是在每次需要创建 GoodWorkerState 实例时,都去调用 list() 这个函数来生成一个全新的、独立的空列表。” 这就解决了共享问题。
WorkerState 中使用 default=None 加上 __post_init__ 的方法,其效果与 default_factory 完全相同,都是为了确保每个实例拥有自己独立的可变对象属性。
更多推荐


所有评论(0)