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

ToonMe
ToonMe

一款风靡Instagram的软件,一键生成卡通头像

下载

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

相关专题

更多
python开发工具
python开发工具

php中文网为大家提供各种python开发工具,好的开发工具,可帮助开发者攻克编程学习中的基础障碍,理解每一行源代码在程序执行时在计算机中的过程。php中文网还为大家带来python相关课程以及相关文章等内容,供大家免费下载使用。

750

2023.06.15

python打包成可执行文件
python打包成可执行文件

本专题为大家带来python打包成可执行文件相关的文章,大家可以免费的下载体验。

635

2023.07.20

python能做什么
python能做什么

python能做的有:可用于开发基于控制台的应用程序、多媒体部分开发、用于开发基于Web的应用程序、使用python处理数据、系统编程等等。本专题为大家提供python相关的各种文章、以及下载和课程。

758

2023.07.25

format在python中的用法
format在python中的用法

Python中的format是一种字符串格式化方法,用于将变量或值插入到字符串中的占位符位置。通过format方法,我们可以动态地构建字符串,使其包含不同值。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

618

2023.07.31

python教程
python教程

Python已成为一门网红语言,即使是在非编程开发者当中,也掀起了一股学习的热潮。本专题为大家带来python教程的相关文章,大家可以免费体验学习。

1262

2023.08.03

python环境变量的配置
python环境变量的配置

Python是一种流行的编程语言,被广泛用于软件开发、数据分析和科学计算等领域。在安装Python之后,我们需要配置环境变量,以便在任何位置都能够访问Python的可执行文件。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

547

2023.08.04

python eval
python eval

eval函数是Python中一个非常强大的函数,它可以将字符串作为Python代码进行执行,实现动态编程的效果。然而,由于其潜在的安全风险和性能问题,需要谨慎使用。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

577

2023.08.04

scratch和python区别
scratch和python区别

scratch和python的区别:1、scratch是一种专为初学者设计的图形化编程语言,python是一种文本编程语言;2、scratch使用的是基于积木的编程语法,python采用更加传统的文本编程语法等等。本专题为大家提供scratch和python相关的文章、下载、课程内容,供大家免费下载体验。

706

2023.08.11

php与html混编教程大全
php与html混编教程大全

本专题整合了php和html混编相关教程,阅读专题下面的文章了解更多详细内容。

12

2026.01.13

热门下载

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

精品课程

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

共4课时 | 0.6万人学习

Django 教程
Django 教程

共28课时 | 3.1万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.1万人学习

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

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