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

热门广告位

Python 类定义中可变属性的陷阱:为何列表会意外共享与重复

Python 类定义中可变属性的陷阱:为何列表会意外共享与重复

当在 Python 类定义中直接初始化可变类型(如列表)作为属性时,所有实例会共享同一个列表对象。这可能导致数据意外累积或重复,尤其在多次实例化或特定运行环境下(如控制台运行或集成测试)。为避免此问题,应在类的 __init__ 方法中初始化可变实例属性,确保每个对象拥有独立的属性副本,从而维护数据隔离性和预期行为。本文将深入探讨这一常见陷阱,分析其根本原因,并提供专业的解决方案和最佳实践。

1. 问题现象:测试中列表数据意外翻倍

在进行 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 方法内部被显式初始化为新的空列表。

2. 根本原因:Python 类属性与实例属性的混淆

这种现象的根源在于 Python 中类属性和实例属性的工作机制,特别是当类属性被赋予可变默认值时。

2.1 类属性与实例属性

  • 类属性 (Class Attributes): 在类定义体中直接定义的属性,它们属于类本身,并由该类的所有实例共享。当一个类属性被修改时,所有实例都会看到这个修改。
  • 实例属性 (Instance Attributes): 在 __init__ 方法或其他实例方法中,通过 self.attribute_name 定义的属性。每个实例都有其独立的副本,一个实例对自身实例属性的修改不会影响其他实例。

2.2 可变默认参数陷阱

当一个可变对象(如列表 []、字典 {}、集合 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 追加数据时,它们实际上都在向同一个列表追加,导致数据累积和重复。

家作

家作

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

家作38

查看详情
家作

而 legs_and_phase 列表之所以没有问题,是因为在 __extract_leg_and_phase 方法中,self.legs_and_phase = [] 这行代码总是会为当前实例创建一个新的空列表,并将其赋值给 self.legs_and_phase,从而覆盖了任何可能的类属性引用,确保了每个实例都拥有独立的列表副本。

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

解决这个问题的关键在于,确保每个类实例都拥有其独立的、不与其他实例共享的可变属性副本。这通常通过在类的构造函数 __init__ 方法中显式地初始化这些属性来实现。

3.1 正确做法

将所有可变实例属性的初始化逻辑从类定义体移动到 __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 创建全新的、独立的列表对象。这样,即使创建多个实例,它们各自的列表属性也是相互隔离的,一个实例对自身列表的修改不会影响其他实例,从而彻底解决了数据重复的问题。

4. 最佳实践与注意事项

为了避免未来再次遇到类似的问题,请遵循以下最佳实践:

  • 通用原则: 永远不要在类定义中将可变对象(如列表、字典、集合)作为默认值。这些默认值只在类加载时创建一次,并被所有实例共享。
  • 不可变默认值是安全的: 对于不可变对象(如数字、字符串、元组、None),作为类属性的默认值通常是安全的,因为它们的值无法被修改,只能被重新绑定。例如:

    class MyClass:
    count = 0  # 不可变,共享是安全的
    name = "default" # 不可变,共享是安全的
  • 何时使用类属性:

    • 存储常量(例如 PI = 3.14159)。
    • 存储所有实例共享的配置或元数据。
    • 实现计数器等需要跨实例共享状态的机制(但要注意多线程/并发环境下的同步问题)。
  • 何时使用实例属性:

    • 存储每个实例特有的数据。
    • 所有可变数据结构(列表、字典、集合等)都应作为实例属性在 __init__ 方法中初始化。
  • 调试技巧:

    • 当遇到数据意外累积、状态混乱或测试在不同环境下行为不一致的问题时,首先检查类定义中是否存在可变默认参数。
    • 使用 Python 内置的 id() 函数可以帮助你判断两个变量是否指向内存中的同一个对象。例如,如果你怀疑两个实例共享了一个列表,可以打印 id(instance1.my_list) 和 id(instance2.my_list)。如果 id 值相同,则它们共享同一个对象。

5. 总结

在 Python 编程中,正确区分和使用类属性与实例属性至关重要,尤其是在处理可变数据类型时。将可变对象作为类属性的默认值是一个常见的陷阱,它会导致所有实例意外共享同一个对象,从而引发数据完整性问题。遵循在 __init__ 方法中初始化所有可变实例属性的原则,可以有效避免此类问题,确保每个对象拥有独立的属性副本,从而提升代码的健壮性、可预测性和可维护性。理解这一核心概念是编写高质量 Python 代码的关键一步。

相关标签:

python app session csv 开发环境 Python 数据类型 常量 构造函数 字符串 数据结构 class 线程 多线程 并发 对象

大家都在看:

构建灵活的Python类:使用类方法实现不同初始化方式
Python怎么使用enumerate获取索引和值_enumerate函数索引与值遍历指南
Python循环打印星号图案:从入门到精通
Python 循环打印星号图案:从基础到精通
python中如何将时间戳转换为日期格式_Python时间戳与日期格式相互转换
温馨提示: 本文最后更新于2025-09-24 22:31:59,某些文章具有时效性,若有错误或已失效,请在下方留言或联系在线客服
文章版权声明 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
喜欢就支持一下吧
点赞10赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容