28、面向对象与数据类:dataclasses模块详解,NamedTuple,不可变对象设计

说到数据类,我得先坦白一件事。

早些年写 Python,我经常为了一个简单的数据容器,写一堆 __init____repr____eq__ 方法。说实话,又臭又长。后来 dataclasses 一出来,我整个人都清爽了。今天我们就来聊聊这个模块,顺便把 NamedTuple 和不可变对象设计也一并讲透。

为什么需要数据类?

你想想看,一个普通的类,如果只是用来存数据,你得写多少样板代码?

class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"Person(name={self.name!r}, age={self.age!r})"

    def __eq__(self, other):
        if not isinstance(other, Person):
            return NotImplemented
        return self.name == other.name and self.age == other.age

这才三个字段,代码已经快20行了。要是字段再多一点,你写起来不烦,我看都看累了。

dataclasses 就是来解决这个问题的。它用装饰器的方式,自动帮你生成 __init____repr____eq__ 等方法。说白了,你只需要声明字段,剩下的交给它。

核心思想:数据类就是专门用来存储数据的类。它不关心行为,只关心数据本身。

dataclasses 模块详解

先看一个最简单的例子:

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

就这两行,上面那一大堆代码的功能全有了。你可以直接 Person("张三", 25) 创建对象,打印出来也是清晰的格式,两个对象也能直接比较相等。

我个人习惯,只要一个类的主要职责是「装数据」,我就会用 @dataclass。它让代码变得非常干净。

常用参数

@dataclass 装饰器有几个参数,我挑最常用的说:

参数 默认值 说明
init True 是否生成 __init__
repr True 是否生成 __repr__
eq True 是否生成 __eq__
order False 是否生成比较方法(<, > 等)
frozen False 是否创建不可变对象

举个例子,如果你想让对象支持排序:

@dataclass(order=True)
class Student:
    name: str
    score: int

s1 = Student("小明", 90)
s2 = Student("小红", 85)
print(s1 > s2)  # True

字段的默认值与类型

字段可以设置默认值,也可以使用 field 函数做更精细的控制:

from dataclasses import dataclass, field

@dataclass
class Config:
    host: str = "localhost"
    port: int = 8080
    tags: list = field(default_factory=list)  # 避免可变默认值陷阱

注意:不要用 tags: list = [] 这种写法。所有实例会共享同一个列表,这是 Python 的一个经典陷阱。用 default_factory=list 才是正确做法。

我曾经在项目里见过有人因为这个 bug 排查了一整天。两个不同的配置对象,改了其中一个的 tags,另一个也跟着变了。嗯,这种坑踩过一次就记住了。

NamedTuple:轻量级数据容器

如果你只需要一个简单的、不可变的数据结构,NamedTuple 可能比 dataclass 更合适。

from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float

p = Point(3.0, 4.0)
print(p.x, p.y)  # 3.0 4.0
print(p[0])      # 3.0 也支持索引访问

NamedTuple 本质上是元组的子类。它既有元组的轻量和不可变性,又有关键字访问的便利性。

特性 dataclass NamedTuple
可变性 默认可变,可设置 frozen=True 不可变
性能 普通类性能 接近元组,更轻量
继承 支持 有限支持
方法定义 可以定义任意方法 可以定义,但较少用
索引访问 不支持 支持

我个人建议:如果数据量很大,或者你需要频繁创建和销毁对象,用 NamedTuple。如果数据类需要一些行为(比如方法、属性),用 dataclass

不可变对象设计

为什么要设计不可变对象?

说白了,就是为了安全。一个对象一旦创建,它的状态就不能再改变。这在多线程环境下特别有用——你不用担心一个线程在修改数据,另一个线程在读数据,结果读到一半的数据。

dataclasses 中,设置 frozen=True 就能得到不可变对象:

@dataclass(frozen=True)
class ImmutablePoint:
    x: float
    y: float

p = ImmutablePoint(1.0, 2.0)
# p.x = 3.0  # 会报错:FrozenInstanceError

但是要注意,frozen 只保证字段引用不可变。如果字段本身是可变对象(比如列表),那内容还是可以改的:

@dataclass(frozen=True)
class Team:
    name: str
    members: list

t = Team("开发组", ["张三"])
t.members.append("李四")  # 不会报错!列表内容变了

技巧:要实现真正的深度不可变,字段类型也要用不可变类型。比如用 tuple 代替 list,用 frozenset 代替 set

知识体系结构图

下面这张图梳理了本章的核心内容:

Python 数据类方案 dataclass 自动生成 __init__ 自动生成 __repr__ 自动生成 __eq__ 支持 frozen 不可变 支持 field 精细控制 NamedTuple 继承自 tuple 天然不可变 支持索引访问 更轻量、性能更好 适合简单数据容器 不可变对象设计 frozen=True 实现浅不可变 配合不可变类型实现深不可变

实战建议

最后,我分享几个实际项目中的经验:

  • 配置类用 dataclass:配置通常需要可变,而且字段多,dataclass 的默认值功能很方便。
  • 数据传输对象用 NamedTuple:比如 API 返回的数据结构,不可变更安全,而且可以解包。
  • 多线程共享数据用不可变对象:设置 frozen=True,配合 tuplefrozenset 等不可变类型。
  • 不要滥用:如果一个类有复杂的行为逻辑,还是老老实实写普通类吧。数据类只适合「装数据」的场景。

一句话总结:数据类让你的代码更简洁,不可变对象让你的代码更安全。两者结合,事半功倍。

好了,这一章就到这里。记住,工具是为人服务的,别为了用而用。选最适合你场景的方案,才是正道。


公众号:蓝海资料掘金营,微信 deep3321