
在python开发中,我们有时会遇到一种看似奇怪的现象:一段测试代码在集成开发环境(ide)中运行正常,但通过命令行(如pytest)执行时却出现断言失败,具体表现为某些列表的长度翻倍。这通常发生在类中的可变数据结构(如列表)被意外地在多个实例之间共享时。
以下是一个典型的测试场景和相关代码:
import os
from datetime import datetime
from io import StringIO
import pandas
from pandas import DataFrame
# 假设 FhdbTsvDecoder 是待测试的类
# ... (FHD_TIME_FORMAT 和 extract_tsv_from_zip 等定义)
class TestExtractLegsAndPhase:
@staticmethod
def extract_tsv() -> str:
path: str = (os.path.dirname(os.path.realpath(__file__))
+ "/resources/FPFaultHistory.zip")
print("extracting from " + path)
# 假设 extract_tsv_from_zip 是一个从zip文件提取TSV字符串的函数
return "col1\tcol2\tcol3\tcol4\t01/26/2023 07:42:07\t5\t6\n" \
"0\t0\t0\t0\t01/26/2023 07:42:07\t0\t0\n" \
"col1\tcol2\tcol3\tcol4\t01/26/2023 09:48:13\t5\t6\n" \
"0\t0\t0\t0\t01/26/2023 09:48:13\t0\t0\n" # 示例数据
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 列表的长度会变成预期的两倍(例如,57变为114),导致断言失败。然而,legs_and_phase 列表的长度却始终正确。通过调试发现,这些列表中的数据仅仅是简单地重复了一次。
问题的核心在于Python中类属性和实例属性的初始化方式,特别是涉及到可变对象(如列表、字典)时。
考虑以下 FhdbTsvDecoder 类的简化版本:
立即学习“Python免费学习笔记(深入)”;
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
# self.session_starts = [] # 修正方案:在此处初始化
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)
self.legs_and_phase = [] # 在方法内部初始化,每次调用都会创建新列表
# self.session_ends = [] # 修正方案:在此处初始化,如果未在__init__中完成
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])在Python中:
对于 session_starts: list[datetime] = [],列表 [] 是一个可变对象。当多个 FhdbTsvDecoder 实例被创建时(例如,在不同的测试用例或集成测试中),它们都引用同一个 [] 列表。如果一个实例修改了这个列表(例如,通过 append 方法),所有其他实例都会看到这些修改。这导致了数据在实例之间被意外共享和累积。
legs_and_phase 之所以没有这个问题,是因为它在 __extract_leg_and_phase 方法内部被显式地重新初始化为 self.legs_and_phase = []。这意味着每次调用该方法时,都会为当前的实例创建一个新的、空的列表,从而避免了共享问题。
至于为什么在IDE和控制台运行时表现不同,这通常与测试框架(如pytest)的运行机制有关。在某些情况下,尤其是在大型测试套件或集成测试中,类可能在不同的测试运行之间被重用或以某种方式保持状态,导致类级别的共享可变对象累积数据。例如,如果一个集成测试先运行并创建了 FhdbTsvDecoder 实例,它会向共享的 session_starts 列表添加数据。随后,单元测试运行时创建的 FhdbTsvDecoder 实例会继承这个已经包含数据的列表,导致数据翻倍。
解决此问题的关键在于确保每个类实例都拥有其可变属性的独立副本。这通过在类的 __init__ 方法中初始化这些属性来实现。
将 session_starts 和 session_ends 的初始化从类级别移动到 __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
# 确保每个实例都有自己独立的列表对象
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__ 中已经初始化,这里可以省略,或者仅作为额外的清空/重新初始化逻辑
# self.legs_and_phase = [] # 根据需求决定是否需要在此处重新初始化
# self.session_starts = [] # 如果在__init__中初始化,此处不需要
# self.session_ends = [] # 如果在__init__中初始化,此处不需要
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 创建全新的、独立的列表对象。这样,即使在不同的测试运行或多个实例之间,这些列表也不会相互影响,从而解决了数据累积和断言失败的问题。
可变对象始终在 __init__ 中初始化: 这是Python面向对象编程中的一条黄金法则。对于任何需要每个实例拥有独立状态的可变属性(如列表、字典、集合等),务必在 __init__ 方法中进行初始化。
class MyClass:
# 错误示例:可变类属性,所有实例共享
shared_list = []
# 正确示例:在__init__中初始化实例属性
def __init__(self):
self.instance_list = []何时使用类属性: 类属性适用于存储:
避免函数默认可变参数的陷阱: 与类属性类似,Python函数定义中默认参数如果设置为可变对象,也会导致类似的问题。
def add_item(item, my_list=[]): # 错误:my_list在函数定义时只创建一次
my_list.append(item)
return my_list
print(add_item(1)) # 输出: [1]
print(add_item(2)) # 输出: [1, 2] - 意外地保留了之前的状态
def add_item_correct(item, my_list=None):
if my_list is None:
my_list = []
my_list.append(item)
return my_list
print(add_item_correct(1)) # 输出: [1]
print(add_item_correct(2)) # 输出: [2] - 每次调用都创建新列表测试隔离的重要性: 在编写测试时,应确保每个测试用例都是独立的,不依赖于其他测试用例的副作用。理解Python的类属性行为有助于避免因意外的数据共享而导致的测试不稳定。如果测试框架在不同测试之间重用模块或类,这种共享问题会更加突出。
Python中可变类属性的意外共享是一个常见的陷阱,尤其是在涉及列表、字典等可变数据结构时。当在类级别初始化这些可变对象时,所有实例将引用同一个对象,导致数据污染和难以调试的错误。解决之道是在类的 __init__ 方法中为每个实例创建独立的属性副本。遵循这一最佳实践,可以显著提高代码的健壮性、可预测性,并避免在测试和生产环境中出现因数据累积而导致的异常行为。
以上就是Python中可变类属性的风险与正确初始化方法的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号