值得一看
广告
彩虹云商城
广告

热门广告位

Python中可变类属性的风险与正确初始化方法

Python中可变类属性的风险与正确初始化方法

本文探讨了Python中因类级别初始化可变数据结构(如列表)而导致的实例间数据共享问题。当此类属性在类定义时被赋值为可变对象时,所有实例将共享同一个对象,导致数据意外累积。解决方案是在类的 __init__ 方法中初始化这些可变属性,确保每个实例拥有独立的副本,从而避免在多实例场景(如测试)中出现数据污染。

问题描述:测试环境中的异常行为

在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类属性与实例属性的混淆

问题的核心在于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中:

  1. 类属性:在类定义体内直接声明的属性(如 session_starts: list[datetime] = [])是类属性。这意味着所有该类的实例都将共享同一个 session_starts 列表对象。这个列表在类加载时只创建一次。
  2. 实例属性:在 __init__ 方法中通过 self.attribute_name = value 声明的属性是实例属性。每个实例都会拥有自己独立的 attribute_name 副本。

对于 session_starts: list[datetime] = [],列表 [] 是一个可变对象。当多个 FhdbTsvDecoder 实例被创建时(例如,在不同的测试用例或集成测试中),它们都引用同一个 [] 列表。如果一个实例修改了这个列表(例如,通过 append 方法),所有其他实例都会看到这些修改。这导致了数据在实例之间被意外共享和累积。

legs_and_phase 之所以没有这个问题,是因为它在 __extract_leg_and_phase 方法内部被显式地重新初始化为 self.legs_and_phase = []。这意味着每次调用该方法时,都会为当前的实例创建一个新的、空的列表,从而避免了共享问题。

家作

家作

淘宝推出的家装家居AI创意设计工具

家作38

查看详情
家作

至于为什么在IDE和控制台运行时表现不同,这通常与测试框架(如pytest)的运行机制有关。在某些情况下,尤其是在大型测试套件或集成测试中,类可能在不同的测试运行之间被重用或以某种方式保持状态,导致类级别的共享可变对象累积数据。例如,如果一个集成测试先运行并创建了 FhdbTsvDecoder 实例,它会向共享的 session_starts 列表添加数据。随后,单元测试运行时创建的 FhdbTsvDecoder 实例会继承这个已经包含数据的列表,导致数据翻倍。

解决方案:在 __init__ 方法中初始化实例属性

解决此问题的关键在于确保每个类实例都拥有其可变属性的独立副本。这通过在类的 __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 创建全新的、独立的列表对象。这样,即使在不同的测试运行或多个实例之间,这些列表也不会相互影响,从而解决了数据累积和断言失败的问题。

最佳实践与注意事项

  1. 可变对象始终在 __init__ 中初始化
    这是Python面向对象编程中的一条黄金法则。对于任何需要每个实例拥有独立状态的可变属性(如列表、字典、集合等),务必在 __init__ 方法中进行初始化。

    class MyClass:
    # 错误示例:可变类属性,所有实例共享
    shared_list = []
    # 正确示例:在__init__中初始化实例属性
    def __init__(self):
    self.instance_list = []
  2. 何时使用类属性
    类属性适用于存储:

    • 常量:如 PI = 3.14159。
    • 不可变数据:如元组、字符串或数字。
    • 所有实例共享且不随实例状态变化的属性:例如,一个计数器,记录创建了多少个实例。
  3. 避免函数默认可变参数的陷阱
    与类属性类似,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] - 每次调用都创建新列表
  4. 测试隔离的重要性
    在编写测试时,应确保每个测试用例都是独立的,不依赖于其他测试用例的副作用。理解Python的类属性行为有助于避免因意外的数据共享而导致的测试不稳定。如果测试框架在不同测试之间重用模块或类,这种共享问题会更加突出。

总结

Python中可变类属性的意外共享是一个常见的陷阱,尤其是在涉及列表、字典等可变数据结构时。当在类级别初始化这些可变对象时,所有实例将引用同一个对象,导致数据污染和难以调试的错误。解决之道是在类的 __init__ 方法中为每个实例创建独立的属性副本。遵循这一最佳实践,可以显著提高代码的健壮性、可预测性,并避免在测试和生产环境中出现因数据累积而导致的异常行为。

相关标签:

python app session csv 面向对象编程 python函数 开发环境 为什么 red Python pytest 常量 面向对象 字符串 可变参数 数据结构 继承 append 对象 ide

大家都在看:

解决Selenium Python启动Chrome浏览器SSL证书验证失败问题
构建灵活的Python类:使用类方法实现不同初始化方式
Python怎么使用enumerate获取索引和值_enumerate函数索引与值遍历指南
Python循环打印星号图案:从入门到精通
Python 循环打印星号图案:从基础到精通
温馨提示: 本文最后更新于2025-09-24 22:32:03,某些文章具有时效性,若有错误或已失效,请在下方留言或联系在线客服
文章版权声明 1 本网站名称: 创客网
2 本站永久网址:https://new.ie310.com
1 本文采用非商业性使用-相同方式共享 4.0 国际许可协议[CC BY-NC-SA]进行授权
2 本站所有内容仅供参考,分享出来是为了可以给大家提供新的思路。
3 互联网转载资源会有一些其他联系方式,请大家不要盲目相信,被骗本站概不负责!
4 本网站只做项目揭秘,无法一对一教学指导,每篇文章内都含项目全套的教程讲解,请仔细阅读。
5 本站分享的所有平台仅供展示,本站不对平台真实性负责,站长建议大家自己根据项目关键词自己选择平台。
6 因为文章发布时间和您阅读文章时间存在时间差,所以有些项目红利期可能已经过了,能不能赚钱需要自己判断。
7 本网站仅做资源分享,不做任何收益保障,创业公司上收费几百上千的项目我免费分享出来的,希望大家可以认真学习。
8 本站所有资料均来自互联网公开分享,并不代表本站立场,如不慎侵犯到您的版权利益,请联系79283999@qq.com删除。

本站资料仅供学习交流使用请勿商业运营,严禁从事违法,侵权等任何非法活动,否则后果自负!
THE END
喜欢就支持一下吧
点赞13赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容