
在进行 python 单元测试时,开发者可能会遇到一种奇怪的现象:某些列表属性在集成开发环境(如 intellij)中运行测试时表现正常,但在控制台直接运行或在集成测试中被多次实例化时,其长度会意外翻倍,内容也随之重复。
例如,考虑以下测试代码片段:
# 示例测试代码片段
import os
from datetime import datetime
from io import StringIO
import pandas
from pandas import DataFrame
FHD_TIME_FORMAT = '%m/%d/%Y %H:%M:%S'
# 假设 FhdbTsvDecoder 是待测试的类
# 简化后的 FhdbTsvDecoder 类定义,其中包含问题代码
class FhdbTsvDecoder:
tsv: str
legs_and_phase: list[tuple[datetime, int, int]]
session_starts: list[datetime] = [] # 问题所在:在类级别初始化可变列表
session_ends: list[datetime] # 另一个潜在问题,如果不在 __init__ 中初始化
def __init__(self, tsv: str):
self.tsv = tsv
# self.session_starts = [] # 如果在此处初始化,则正常
# self.session_ends = [] # 如果在此处初始化,则正常
self.__extract_leg_and_phase()
def __extract_leg_and_phase(self) -> None:
df: DataFrame = pandas.read_csv(StringIO(self.tsv), sep='\t', header=None,
converters={4: lambda x: datetime.strptime(x, FHD_TIME_FORMAT)},
skiprows=0)
# 此处初始化 legs_and_phase,使其每次都是新的实例属性
self.legs_and_phase = []
# 如果 session_starts 和 session_ends 在 __init__ 中未初始化,
# 且在类级别被初始化为共享列表,则此处操作的是共享列表
# self.session_starts = [] # 如果在此处初始化,则正常
self.session_ends = [] # 此处初始化,使其每次都是新的实例属性
iterator = df.iterrows()
for index, row in iterator:
list.append(self.legs_and_phase, (row[4], row[5], row[6]))
if row[1] == row[2] == row[3] == row[5] == row[6] == 0:
self.session_ends.append(row[4])
# 注意:next(iterator) 会消耗下一行数据
self.session_starts.append(next(iterator)[1][4])
class TestExtractLegsAndPhase:
# 假设 extract_tsv() 和 extract_tsv_from_zip() 已定义并返回有效的TSV字符串
@staticmethod
def extract_tsv() -> str:
# 实际路径和内容省略
return "mock_tsv_content"
tsv: str = extract_tsv()
def test_extract_leg_and_phase(self):
to: FhdbTsvDecoder = FhdbTsvDecoder(self.tsv)
legs_and_phase: list[tuple[datetime, int, int]] = to.legs_and_phase
assert len(legs_and_phase) == 4926 # 始终通过
session_ends: list[datetime] = to.session_ends
assert len(session_ends) == 57 # 在控制台运行时可能失败,实际为114
session_starts: list[datetime] = to.session_starts
assert len(session_starts) == 57 # 在控制台运行时可能失败,实际为114在上述例子中,session_ends 和 session_starts 列表的断言在控制台运行时可能会失败,其长度显示为 114 而非预期的 57,内容是原始数据的重复。然而,legs_and_phase 列表的断言却始终通过。进一步的调试发现,问题在于 session_starts 列表在类定义时被初始化,而 legs_and_phase 则在 __extract_leg_and_phase 方法内部被显式初始化为新的空列表。
这种现象的根源在于 Python 中类属性和实例属性的工作机制,特别是当类属性被赋予可变默认值时。
当一个可变对象(如列表 []、字典 {}、集合 set())被用作类属性的默认值时,这个可变对象在类被定义和加载时只创建一次。所有该类的实例,如果它们没有在 __init__ 方法中显式地为该属性创建新的实例级副本,就会引用这个同一个共享的可变对象。
立即学习“Python免费学习笔记(深入)”;
在我们的例子中:
class FhdbTsvDecoder:
# ...
session_starts: list[datetime] = [] # 问题所在
# ...这行代码在 FhdbTsvDecoder 类被加载到内存时,创建了一个空的列表对象,并将其赋值给 FhdbTsvDecoder.session_starts 这个类属性。每次创建 FhdbTsvDecoder 的新实例时,如果 __init__ 方法没有显式地为 self.session_starts 赋值一个新的列表,那么 self.session_starts 将会引用这个由所有实例共享的类属性列表。
当测试或集成测试创建了多个 FhdbTsvDecoder 实例(例如,一个集成测试运行后又运行了另一个测试,或者测试框架在不同阶段创建了实例),并且这些实例都调用 __extract_leg_and_phase 方法向 self.session_starts 追加数据时,它们实际上都在向同一个列表追加,导致数据累积和重复。
而 legs_and_phase 列表之所以没有问题,是因为在 __extract_leg_and_phase 方法中,self.legs_and_phase = [] 这行代码总是会为当前实例创建一个新的空列表,并将其赋值给 self.legs_and_phase,从而覆盖了任何可能的类属性引用,确保了每个实例都拥有独立的列表副本。
解决这个问题的关键在于,确保每个类实例都拥有其独立的、不与其他实例共享的可变属性副本。这通常通过在类的构造函数 __init__ 方法中显式地初始化这些属性来实现。
将所有可变实例属性的初始化逻辑从类定义体移动到 __init__ 方法中。
from datetime import datetime
from io import StringIO
import pandas
from pandas import DataFrame
FHD_TIME_FORMAT = '%m/%d/%Y %H:%M:%S'
class FhdbTsvDecoder:
tsv: str
legs_and_phase: list[tuple[datetime, int, int]]
session_starts: list[datetime]
session_ends: list[datetime]
def __init__(self, tsv: str):
self.tsv = tsv
# 在 __init__ 方法中初始化所有可变实例属性
self.legs_and_phase = []
self.session_starts = []
self.session_ends = []
self.__extract_leg_and_phase()
def __extract_leg_and_phase(self) -> None:
df: DataFrame = pandas.read_csv(StringIO(self.tsv), sep='\t', header=None,
converters={4: lambda x: datetime.strptime(x, FHD_TIME_FORMAT)},
skiprows=0)
# 移除或调整方法内部的列表初始化,因为它们已在 __init__ 中完成
# 如果方法可能被多次调用且需要清空列表,则可以保留清空逻辑
# 但首次初始化应由 __init__ 负责
# self.legs_and_phase = [] # 如果 __init__ 中已初始化,此处可移除或改为 clear()
# self.session_starts = [] # 移除此行
# self.session_ends = [] # 移除此行
iterator = df.iterrows()
for index, row in iterator:
list.append(self.legs_and_phase, (row[4], row[5], row[6]))
if row[1] == row[2] == row[3] == row[5] == row[6] == 0:
self.session_ends.append(row[4])
self.session_starts.append(next(iterator)[1][4])
通过上述修改,每次创建 FhdbTsvDecoder 实例时,__init__ 方法都会为 self.legs_and_phase、self.session_starts 和 self.session_ends 创建全新的、独立的列表对象。这样,即使创建多个实例,它们各自的列表属性也是相互隔离的,一个实例对自身列表的修改不会影响其他实例,从而彻底解决了数据重复的问题。
为了避免未来再次遇到类似的问题,请遵循以下最佳实践:
class MyClass:
count = 0 # 不可变,共享是安全的
name = "default" # 不可变,共享是安全的在 Python 编程中,正确区分和使用类属性与实例属性至关重要,尤其是在处理可变数据类型时。将可变对象作为类属性的默认值是一个常见的陷阱,它会导致所有实例意外共享同一个对象,从而引发数据完整性问题。遵循在 __init__ 方法中初始化所有可变实例属性的原则,可以有效避免此类问题,确保每个对象拥有独立的属性副本,从而提升代码的健壮性、可预测性和可维护性。理解这一核心概念是编写高质量 Python 代码的关键一步。
以上就是Python 类定义中可变属性的陷阱:为何列表会意外共享与重复的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号