0

0

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

碧海醫心

碧海醫心

发布时间:2025-09-24 22:07:22

|

595人浏览过

|

来源于php中文网

原创

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 方法),所有其他实例都会看到这些修改。这导致了数据在实例之间被意外共享和累积。

Figma
Figma

Figma 是一款基于云端的 UI 设计工具,可以在线进行产品原型、设计、评审、交付等工作。

下载

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

至于为什么在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__ 方法中为每个实例创建独立的属性副本。遵循这一最佳实践,可以显著提高代码的健壮性、可预测性,并避免在测试和生产环境中出现因数据累积而导致的异常行为。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1500

2023.10.24

go语言 面向对象
go语言 面向对象

本专题整合了go语言面向对象相关内容,阅读专题下面的文章了解更多详细内容。

56

2025.09.05

java面向对象
java面向对象

本专题整合了java面向对象相关内容,阅读专题下面的文章了解更多详细内容。

52

2025.11.27

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

298

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

212

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1500

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

623

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

613

2024.03.22

Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

22

2026.01.27

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 22.3万人学习

Django 教程
Django 教程

共28课时 | 3.6万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号