欢迎光临
我们一直在努力

《流畅的Python》读书笔记06: 第一部分 数据结构 - 数据类构建器

作者: andylin02
学习章节: 第 5 章 数据类构建器
关键词: 数据类构建器|dataclass|namedtuple|typing.NamedTuple|field|default_factory|frozen|__post_init__|类型注解|不可变对象|数据类代码异味|值对象|DTO


一、本章概述

《流畅的 Python》第 5 章“数据类构建器”(Data Class Builders)是第二版中全新添加的一章。数据类(Data Class)是一种“只包含字段(属性)和少量逻辑的类”,其典型特征是主要职责是承載数据、几乎不写样板代码(boilerplate)、自动生成常用方法。

在 Python 中,当我们需要一个简单的、只存储数据的类时,传统方式需要手动编写 __init__、__repr__、__eq__ 等方法,写法冗长且容易出错。如果有超过几个属性,每个属性都被提及三次,而且默认从 object 继承的 __repr__ 并不能提供有用的信息,__eq__ 也仅仅比较对象 ID 而非实际内容。

为了解决这些问题,Python 提供了三种不同的类构建器作为快捷方式:

  • collections.namedtuple:最简单的方式,自 Python 2.6 起可用。
  • typing.NamedTuple:一种需要在字段上添加类型提示的替代方法,自 Python 3.5 起可用,3.6 中添加了 class 语法。
  • @dataclasses.dataclass:一个类装饰器,比之前的方案允许更多定制,增加了许多选项和潜在的复杂性,自 Python 3.7 起可用。

需要特别说明的是:typing.TypedDict 看起来可能像另一个数据类构建器,但它不会构建可以实例化的具体类,只是为接受字典类型参数的函数和变量编写类型提示的语法,将在本书第 15 章中介绍。

本章在介绍了这三种类构建器之后,还讨论了为什么“数据类”也是一种代码异味——一种可能表示糟糕面向对象设计的编码模式。

二、为什么需要数据类构建器

在了解具体的数据类构建器之前,先通过一个普通的 Python 类来看传统方式存在的问题。

假设我们需要一个简单的坐标类:

class Coordinate:
def __init__(self, lat, lon):
self.lat = lat
self.lon = lon

首先,编写 __init__ 样板变得相当乏味,特别是如果类有多个属性。其次,这个类并没有提供 Python 对象应当具备的基本功能:

moscow = Coordinate(55.76, 37.62)
print(moscow) # <__main__.Coordinate at 0x107142f10> —— 难以阅读的输出
loc = Coordinate(55.76, 37.62)
print(moscow == loc) # False —— 对象内容相同却不相等
print(moscow.lat == loc.lat and moscow.lon == loc.lon) # True —— 只能逐个比较属性

问题在于:

  • __repr__ 从 object 继承,不是特别有用
  • __eq__ 从 object 继承,比较的是对象 ID 而不是值
  • 比较两个坐标需要显式逐个比较每个属性

这正是数据类构建器要解决的问题。它们会自动提供必要的 __init__、__repr__ 和 __eq__ 方法,以及其他有用的功能。

三、三种数据类构建器详解

3.1 collections.namedtuple:最轻量级的数据容器

namedtuple 是在 Python 2.6 中引入的工厂函数,用于构建 tuple 的子类,同时为每个字段提供名称。它的实例与普通元组一样消耗内存,且完全不可变。

from collections import namedtuple

# 基础用法
Coordinate = namedtuple('Coordinate', 'lat lon')
moscow = Coordinate(55.756, 37.617)
print(moscow) # Coordinate(lat=55.756, lon=37.617)
print(f"纬度: {moscow.lat}") # 通过属性名访问
print(f"经度: {moscow[1]}") # 通过索引访问(兼容元组)
print(issubclass(Coordinate, tuple)) # 输出: True

核心特性

特性说明
不可变性 实例是元组的子类,不可直接修改字段值
字段名访问 通过属性名(如 obj.x)而非索引访问数据,提升可读性
默认值支持 Python 3.7+ 允许为字段设置默认值
序列化友好 通过 ._asdict() 转为字典,便于 JSON 序列化
内存高效 与普通元组一样基于 C 实现,内存占用极小

适用场景:需要不可变数据且无需复杂逻辑的简单场景(如坐标点、配置项)。

设置默认值的方式

namedtuple 从 Python 3.7 开始支持默认值:

from collections import namedtuple

# 通过 defaults 参数设置默认值
Person = namedtuple('Person', ['name', 'age', 'city'], defaults=['Beijing', 18])
p1 = Person(name='Alice', age=25)
print(p1) # Person(name='Alice', age=25, city='Beijing')
p2 = Person('Bob', 30, 'Shanghai')
print(p2) # Person(name='Bob', age=30, city='Shanghai')

_replace() 方法——生成修改后的副本

由于 namedtuple 是不可变的,如果需要“修改”某个字段,只能通过 _replace() 方法返回一个新的实例:

Coordinate = namedtuple('Coordinate', 'lat lon')
p1 = Coordinate(10, 20)

# 要“修改” x 坐标的值,返回一个新实例
p2 = p1._replace(lat=30)
print(p1) # Coordinate(lat=10, lon=20) —— 原实例不变
print(p2) # Coordinate(lat=30, lon=20) —— 新实例

继承 namedtuple 添加方法

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])

class Point2D(Point):
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def distance(self):
return (self.x ** 2 + self.y ** 2) ** 0.5

p1 = Point2D(10, 20)
p2 = Point2D(15, 35)
print(p1 + p2) # Point(x=25, y=55)
print(p1.distance()) # 22.360679774997898

3.2 typing.NamedTuple:带类型注解的现代命名元组

typing.NamedTuple 在 Python 3.5 中引入,和 collections.namedtuple 构建的是同一个概念——都是 tuple 的子类。区别在于它要求为每个字段添加类型注解(仅在类型检查器中使用,运行时无检验),并且从 Python 3.6 开始支持 class 语句句法。

函数式创建方式

import typing

Coordinate = typing.NamedTuple('Coordinate', [('lat', float), ('lon', float)])
moscow = Coordinate(55.756, 37.617)
print(moscow) # Coordinate(lat=55.756, lon=37.617)
print(typing.get_type_hints(Coordinate)) # {'lat': <class 'float'>, 'lon': <class 'float'>}

class 语句语法(推荐方式,Python 3.6+)

from typing import NamedTuple

class Coordinate(NamedTuple):
lat: float
lon: float

def __str__(self):
ns = 'N' if self.lat >= 0 else 'S'
we = 'E' if self.lon >= 0 else 'W'
return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'

coord = Coordinate(55.756, 37.617)
print(coord) # 55.8°N, 37.6°E
print(issubclass(Coordinate, tuple)) # True
print(issubclass(Coordinate, NamedTuple)) # False —— 不是继承关系,是元类魔法

类型注解与默认值结合

from typing import NamedTuple, Optional

class Employee(NamedTuple):
name: str
age: int = 30 # 默认值
department: Optional[str] = None # 可选字段

alice = Employee('Alice', 25, 'Engineering')
bob = Employee('Bob') # age 使用默认值 30
print(alice) # Employee(name='Alice', age=25, department='Engineering')
print(bob) # Employee(name='Bob', age=30, department=None)

3.3 @dataclasses.dataclass:功能最丰富的数据类

@dataclass 是 Python 3.7 中引入的装饰器,提供了比前两种方案更多的自定义选项,包括可进行完全定制、支持可变性,并依赖专门用于处理数据类的 dataclasses 模块。

注意:与两种具名元组不同,@dataclass 不会有任何继承关系,因此不会影响类层次结构。

最基础的用法

from dataclasses import dataclass

@dataclass
class Coordinate:
lat: float
lon: float

moscow = Coordinate(55.756, 37.617)
print(moscow) # Coordinate(lat=55.756, lon=37.617)
moscow.lat = 60.0 # 默认是可变的,可以直接修改
print(moscow) # Coordinate(lat=60.0, lon=37.617)

@dataclass 会自动为类生成 __init__、__repr__、__eq__ 等方法,还可以配置是否生成 __hash__ 及排序等相关方法。

设置默认值

from dataclasses import dataclass

@dataclass
class Person:
name: str
age: int = 18 # 字段默认值
city: str = 'Beijing' # 字段默认值

p1 = Person('Alice') # 使用默认值
p2 = Person('Bob', 25, 'Shanghai') # 覆盖默认值
print(p1) # Person(name='Alice', age=18, city='Beijing')
print(p2) # Person(name='Bob', age=25, city='Shanghai')

field() 与 default_factory——处理可变默认值

在 Python 中,可变类型(如列表、字典)不能直接作为函数参数的默认值,否则会在所有实例之间共享该对象。@dataclass 通过 field(default_factory=list) 来解决这个问题,每次创建对象时调用 default_factory 生成一个新实例:

from dataclasses import dataclass, field

@dataclass
class ShoppingCart:
items: list = field(default_factory=list) # 正确做法
# items: list = [] # ❌ 陷阱 —— 所有实例共享同一个列表!

cart1 = ShoppingCart()
cart2 = ShoppingCart()
cart1.items.append('apple')
print(cart1.items) # ['apple']
print(cart2.items) # [ ] —— 独立实例,不受影响

field() 支持的参数:

  • default:默认值
  • default_factory:用于生成默认值的 0 参数可调用对象
  • init:是否在 __init__ 中包含此字段
  • repr:是否在 __repr__ 中包含此字段
  • compare:是否在比较(__eq__、__lt__ 等)中包含此字段
  • hash:此字段是否被包含在 __hash__ 计算中

frozen=True——创建不可变数据类

默认情况下,@dataclass 创建的是可变类。如果需要不可变对象(类似于 namedtuple),可以设置 frozen=True:

from dataclasses import dataclass

@dataclass(frozen=True)
class Config:
host: str
port: int

config = Config('localhost', 8080)
print(config.port) # 8080
# config.port = 9090 # ❌ 抛出 FrozenInstanceError

post_init 钩子

__post_init__ 是一个特殊方法,在 __init__ 执行完成后自动被调用,适用于需要额外初始化或验证的场景:

from dataclasses import dataclass

@dataclass
class Product:
name: str
price: float
_discount: float = field(repr=False, default=0.0)

def __post_init__(self):
"""在 __init__ 完成后执行额外初始化和验证"""
if self.price < 0:
raise ValueError(f"价格不能为负数: {self.price}")

@property
def final_price(self) > float:
return self.price * (1 self._discount)

# 创建对象
p1 = Product('Laptop', 5999)
print(p1) # Product(name='Laptop', price=5999)
print(p1.final_price) # 5999

p2 = Product('Phone', 3999, 0.1)
print(p2.final_price) # 3599.1

# Product('Invalid', -10) # ❌ 抛出 ValueError

四、三种数据类构建器对比

下表汇总了 collections.namedtuple、typing.NamedTuple 和 @dataclass 的核心差异:

特性namedtupleNamedTupledataclass
可变实例 ❌ 否 ❌ 否 ✅ 是(可配置 frozen=True)
类声明语法 ❌ 否(工厂函数) ✅ 是(class 语句) ✅ 是
构建字典 x._asdict() x._asdict() dataclasses.asdict(x)
获取字段名称 x._fields x._fields [f.name for f in dataclasses.fields(x)]
获取字段默认值 x._field_defaults x._field_defaults [f.default for f in dataclasses.fields(x)]
获取字段类型 ❌ N/A x.__annotations__ x.__annotations__
替换新实例 x._replace(…) x._replace(…) dataclasses.replace(x, …)
运行时创建 namedtuple(…) NamedTuple(…) dataclasses.make_dataclass(…)
内存效率 极高(C 级) 极高(C 级) 较高(纯 Python dict)
Python 版本要求 2.6+ 3.5+(class 语法需 3.6+) 3.7+

关键差异说明:

  • 可变实例:两种具名元组构建的是元组的子类,因此实例都是不可变的。默认 @dataclass 产生可变类,但该装饰器接收一个关键字参数 frozen,设为 True 后会变成不可变类,在尝试为字段赋值时抛出异常。

  • 类声明语法:只有 typing.NamedTuple 和 dataclass 支持 class 语句语法,方便为构建的类添加方法和文档字符串。

  • 构建字典:两种具名元组都提供 .asdict() 实例方法将实例转换为字典。dataclasses 模块提供了 asdict 函数,但这是函数而不是实例方法。

  • _replace 方法:两种具名元组通过 .replace() 返回修改字段后的新实例(适合不可变对象)。dataclasses.replace(x, …) 函数类似,返回的是一个全新实例。

  • 运行时构建新类:三种构建器都支持运行时创建新类——namedtuple、NamedTuple、dataclasses.make_dataclass。这意味着返回值可以在 __init__ 方法内部或函数内部使用。

何时使用哪种

场景推荐方案
极简只读数据容器,兼容 TB 级元组 namedtuple
同上,但需要类型提示工具理解 NamedTuple
数据需要经常修改 dataclass(可变)
数据只读,防止意外修改 @dataclass(frozen=True)
需要字段默认值,且默认值为可变类型 dataclass + field(default_factory=…)
需要自定义 __init__ 之外的初始化逻辑 dataclass + __post_init__
需要运行时生成新类 三种皆可,按需选择
代码被同事频繁 review,需要极度清晰的结构 dataclass(class 语法 + 类型注解)
Python 版本低于 3.7 namedtuple 或 NamedTuple

五、数据类是“代码异味”吗

本章最后讨论了“数据类”一词的另一层含义——Martin Fowler 和 Kent Beck 在《重构》一书中将“Data Class”列为一种代码异味的名称,意指那些只包含字段和访问器(读写字段的 getter、setter 方法),除此之外没有其他功能的类。这类类被称为“愚蠢的数据持有者”,几乎只承担一种责任:存储数据。

当代码中存在大量这样的数据类,且方法分散在不同的模块中时,一旦数据结构发生变化,需要同时修改多处代码,对维护者来说将是一场噩梦。

如何判断数据类是否成为代码异味

不是所有的数据类都是代码异味。以下几种情况判断数据类是否引发了设计层面的问题:

  • 数据类只包含属性,无任何方法——通常是糟糕设计的信号。如果完全没有行为参与且方法分散在多处,可考虑将使用到它的某些方法迁移到数据类内部。
  • 数据类在多个地方被直接操作——可能表示数据类缺失了某些行为,应该重构,把相关行为放进类的内部。
  • 数据类只用于跨层传递数据——在 Python 里数据类的确适合充当轻量级 DTO,但务必控制数量,避免出现过多过程式代码。

最佳实践建议

  • 不要滥用数据类:数据类善于承載数据,但如果某个类在持续积累行为,可以去掉 @dataclass,改成普通类,并手动实现必要的特殊方法。
  • 将行为放入数据类内部:当一个方法频繁使用数据类的某个字段时,考虑把它搬移到数据类本身作为实例方法。
  • DTO(数据传输对象)可以放心使用数据类:在分层架构中,数据类用于表示跨层传递的数据结构,这是合理且推荐的。
  • 关注本章的指引意义:审慎对待数据类,确保代码不会陷入“纯数据 + 外部操作”的陷阱。

    六、实战示例:三种数据类构建器实现同一功能

    以下是一个完整的对比示例,展示三种方式构建相同业务模型的效果。此业务模型用于存储城市信息并提供简单方法。

    from collections import namedtuple
    from typing import NamedTuple
    from dataclasses import dataclass, field

    # 1. collections.namedtuple
    CityNamedTuple = namedtuple('CityNamedTuple', ['name', 'country', 'population', 'area'])

    def is_megacity_namedtuple(city: CityNamedTuple) > bool:
    return city.population > 10000000

    # 2. typing.NamedTuple(带继承的方式添加方法)
    class CityTypingNamedTuple(NamedTuple):
    name: str
    country: str
    population: int
    area: float

    def density(self) > float:
    return self.population / self.area

    def is_megacity(self) > bool:
    return self.population > 10000000

    # 3. dataclass(推荐,灵活性最高)
    @dataclass
    class CityDataClass:
    name: str
    country: str
    population: int
    area: float
    # 通过类属性添加元数据
    tags: list = field(default_factory=list)

    def density(self) > float:
    return self.population / self.area

    def is_megacity(self) > bool:
    return self.population > 10000000

    # 使用示例
    if __name__ == '__main__':
    # namedtuple
    city1 = CityNamedTuple('Tokyo', 'Japan', 37400000, 2194)
    print(f"namedtuple: {city1}")
    print(f"超大都市? {is_megacity_namedtuple(city1)}")

    # typing.NamedTuple
    city2 = CityTypingNamedTuple('Delhi', 'India', 29300000, 1484)
    print(f"typing.NamedTuple: {city2}")
    print(f"人口密度: {city2.density():.1f} 人/km²")

    # dataclass
    city3 = CityDataClass('São Paulo', 'Brazil', 21800000, 1521, tags=['south_america', 'economic_hub'])
    print(f"dataclass: {city3}")
    print(f"超大都市? {city3.is_megacity()}")
    print(f"标签: {city3.tags}")

    七、应用层面选择决策图

    #mermaid-svg-x57dMObzTfUVRXBq{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-x57dMObzTfUVRXBq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-x57dMObzTfUVRXBq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-x57dMObzTfUVRXBq .error-icon{fill:#552222;}#mermaid-svg-x57dMObzTfUVRXBq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-x57dMObzTfUVRXBq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-x57dMObzTfUVRXBq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-x57dMObzTfUVRXBq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-x57dMObzTfUVRXBq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-x57dMObzTfUVRXBq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-x57dMObzTfUVRXBq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-x57dMObzTfUVRXBq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-x57dMObzTfUVRXBq .marker.cross{stroke:#333333;}#mermaid-svg-x57dMObzTfUVRXBq svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-x57dMObzTfUVRXBq p{margin:0;}#mermaid-svg-x57dMObzTfUVRXBq .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-x57dMObzTfUVRXBq .cluster-label text{fill:#333;}#mermaid-svg-x57dMObzTfUVRXBq .cluster-label span{color:#333;}#mermaid-svg-x57dMObzTfUVRXBq .cluster-label span p{background-color:transparent;}#mermaid-svg-x57dMObzTfUVRXBq .label text,#mermaid-svg-x57dMObzTfUVRXBq span{fill:#333;color:#333;}#mermaid-svg-x57dMObzTfUVRXBq .node rect,#mermaid-svg-x57dMObzTfUVRXBq .node circle,#mermaid-svg-x57dMObzTfUVRXBq .node ellipse,#mermaid-svg-x57dMObzTfUVRXBq .node polygon,#mermaid-svg-x57dMObzTfUVRXBq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-x57dMObzTfUVRXBq .rough-node .label text,#mermaid-svg-x57dMObzTfUVRXBq .node .label text,#mermaid-svg-x57dMObzTfUVRXBq .image-shape .label,#mermaid-svg-x57dMObzTfUVRXBq .icon-shape .label{text-anchor:middle;}#mermaid-svg-x57dMObzTfUVRXBq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-x57dMObzTfUVRXBq .rough-node .label,#mermaid-svg-x57dMObzTfUVRXBq .node .label,#mermaid-svg-x57dMObzTfUVRXBq .image-shape .label,#mermaid-svg-x57dMObzTfUVRXBq .icon-shape .label{text-align:center;}#mermaid-svg-x57dMObzTfUVRXBq .node.clickable{cursor:pointer;}#mermaid-svg-x57dMObzTfUVRXBq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-x57dMObzTfUVRXBq .arrowheadPath{fill:#333333;}#mermaid-svg-x57dMObzTfUVRXBq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-x57dMObzTfUVRXBq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-x57dMObzTfUVRXBq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-x57dMObzTfUVRXBq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-x57dMObzTfUVRXBq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-x57dMObzTfUVRXBq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-x57dMObzTfUVRXBq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-x57dMObzTfUVRXBq .cluster text{fill:#333;}#mermaid-svg-x57dMObzTfUVRXBq .cluster span{color:#333;}#mermaid-svg-x57dMObzTfUVRXBq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-x57dMObzTfUVRXBq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-x57dMObzTfUVRXBq rect.text{fill:none;stroke-width:0;}#mermaid-svg-x57dMObzTfUVRXBq .icon-shape,#mermaid-svg-x57dMObzTfUVRXBq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-x57dMObzTfUVRXBq .icon-shape p,#mermaid-svg-x57dMObzTfUVRXBq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-x57dMObzTfUVRXBq .icon-shape .label rect,#mermaid-svg-x57dMObzTfUVRXBq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-x57dMObzTfUVRXBq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-x57dMObzTfUVRXBq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-x57dMObzTfUVRXBq :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    额外注意事项

    强制不可变

    需要可修改

    是且需要不可变

    需要数据容纳类

    需要不可变性吗?

    需要类型提示吗?

    namedtuple

    typing.NamedTuple

    Python 3.7+?

    dataclass支持 field/default_factory

    dataclassfrozen=True

    用普通类或 namedtuple 代替

    初始化后有额外逻辑?

    实现 __post_init__

    无需实现

    八、常见错误与最佳实践

    8.1 常见错误

    错误现象原因解决方案
    多个实例共享了同一个列表 类定义中直接使用可变默认值 使用 field(default_factory=list)
    修改 namedtuple 的字段失败 namedtuple 是不可变的 使用 _replace() 方法返回新实例或改用 dataclass
    序列化时无法转 JSON asdict() 调用错误 dataclass 使用 dataclasses.asdict(obj)
    无法在 __init__ 后执行验证逻辑 未实现 __post_init__ 实现该方法,添加自定义验证
    @dataclass 的 __hash__ 不可用 当手动实现 __eq__ 时,__hash__=None 设置 @dataclass(unsafe_hash=True) 强制计算

    Python 社区推荐偏好:只要 Python 版本高于 3.6,建议使用 typing.NamedTuple 而不是 collections.namedtuple。

    8.2 框架场景下的适用性

    框架/平台推荐方案原因
    FastAPI / Pydantic BaseModel 原生支持数据校验和 OpenAPI
    Django ORM 模型 Django Model 类 集成 ORM 功能,不适合 dataclass
    纯内部数据转换(无需校验) dataclass 性能好,无额外依赖
    SQLAlchemy ORM dataclass(配合映射)或 SQLAlchemy Model 可根据场景灵活选择

    九、本章思维导图

    #mermaid-svg-kvLuHDWTYOKDRvhO{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kvLuHDWTYOKDRvhO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kvLuHDWTYOKDRvhO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kvLuHDWTYOKDRvhO .error-icon{fill:#552222;}#mermaid-svg-kvLuHDWTYOKDRvhO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kvLuHDWTYOKDRvhO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kvLuHDWTYOKDRvhO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kvLuHDWTYOKDRvhO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kvLuHDWTYOKDRvhO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kvLuHDWTYOKDRvhO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kvLuHDWTYOKDRvhO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kvLuHDWTYOKDRvhO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kvLuHDWTYOKDRvhO .marker.cross{stroke:#333333;}#mermaid-svg-kvLuHDWTYOKDRvhO svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kvLuHDWTYOKDRvhO p{margin:0;}#mermaid-svg-kvLuHDWTYOKDRvhO .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-kvLuHDWTYOKDRvhO .cluster-label text{fill:#333;}#mermaid-svg-kvLuHDWTYOKDRvhO .cluster-label span{color:#333;}#mermaid-svg-kvLuHDWTYOKDRvhO .cluster-label span p{background-color:transparent;}#mermaid-svg-kvLuHDWTYOKDRvhO .label text,#mermaid-svg-kvLuHDWTYOKDRvhO span{fill:#333;color:#333;}#mermaid-svg-kvLuHDWTYOKDRvhO .node rect,#mermaid-svg-kvLuHDWTYOKDRvhO .node circle,#mermaid-svg-kvLuHDWTYOKDRvhO .node ellipse,#mermaid-svg-kvLuHDWTYOKDRvhO .node polygon,#mermaid-svg-kvLuHDWTYOKDRvhO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kvLuHDWTYOKDRvhO .rough-node .label text,#mermaid-svg-kvLuHDWTYOKDRvhO .node .label text,#mermaid-svg-kvLuHDWTYOKDRvhO .image-shape .label,#mermaid-svg-kvLuHDWTYOKDRvhO .icon-shape .label{text-anchor:middle;}#mermaid-svg-kvLuHDWTYOKDRvhO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kvLuHDWTYOKDRvhO .rough-node .label,#mermaid-svg-kvLuHDWTYOKDRvhO .node .label,#mermaid-svg-kvLuHDWTYOKDRvhO .image-shape .label,#mermaid-svg-kvLuHDWTYOKDRvhO .icon-shape .label{text-align:center;}#mermaid-svg-kvLuHDWTYOKDRvhO .node.clickable{cursor:pointer;}#mermaid-svg-kvLuHDWTYOKDRvhO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kvLuHDWTYOKDRvhO .arrowheadPath{fill:#333333;}#mermaid-svg-kvLuHDWTYOKDRvhO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kvLuHDWTYOKDRvhO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kvLuHDWTYOKDRvhO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kvLuHDWTYOKDRvhO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kvLuHDWTYOKDRvhO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kvLuHDWTYOKDRvhO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kvLuHDWTYOKDRvhO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kvLuHDWTYOKDRvhO .cluster text{fill:#333;}#mermaid-svg-kvLuHDWTYOKDRvhO .cluster span{color:#333;}#mermaid-svg-kvLuHDWTYOKDRvhO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kvLuHDWTYOKDRvhO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kvLuHDWTYOKDRvhO rect.text{fill:none;stroke-width:0;}#mermaid-svg-kvLuHDWTYOKDRvhO .icon-shape,#mermaid-svg-kvLuHDWTYOKDRvhO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kvLuHDWTYOKDRvhO .icon-shape p,#mermaid-svg-kvLuHDWTYOKDRvhO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kvLuHDWTYOKDRvhO .icon-shape .label rect,#mermaid-svg-kvLuHDWTYOKDRvhO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kvLuHDWTYOKDRvhO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kvLuHDWTYOKDRvhO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kvLuHDWTYOKDRvhO :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    第5章 数据类构建器

    为什么需要

    传统类的问题

    手动__init__繁琐

    __repr__无意义

    __eq__只比较ID

    三种数据类构建器

    collections.namedtuple

    不可变 ✓

    兼容tuple ✓

    字段名访问 ✓

    内存高效 ✓

    不支持类型注解 ✗

    typing.NamedTuple

    不可变 ✓

    类型注解 ✓

    class语法 ✓(3.6+)

    推荐替代旧版

    @dataclass

    默认可变 ✓

    frozen=True → 不可变

    field/default_factory

    __post_init__钩子

    多种定制选项

    Python 3.7 +

    三者的核心差异

    可变性

    class语法

    asdict方法

    _replace

    获取字段元数据

    如何选择合适的

    简单不可变数据 → namedtuple

    需要类型提示 → NamedTuple

    需要修改/定制 → dataclass

    可变默认值 → dataclass + field

    最佳实践

    不要直接使用可变默认值

    初始化后验证用 __post_init__

    警惕 Data Class 代码异味

    为数据类写文档

    十、总结

    第 5 章“数据类构建器”系统介绍了 Python 中用于定义数据容器的三种专用工具。这三种构建器的发展历程清晰地展示了 Python 语言设计的演进:从 2.6 的 namedtuple,到 3.5 的 NamedTuple 添加类型提示,再到 3.7 的 @dataclass 提供丰富的定制能力。

    学习本章的核心收获包括:

    • 减少代码模板:数据类构建器会自动生成 __init__、__repr__、__eq__ 等多种特殊方法,大大减少重复代码。

    • 按需选择:namedtuple 内存效率最高但不可变;NamedTuple 增强了类型提示支持;@dataclass 功能最全面且灵活性最高。实践中,绝大多数项目都会优先选择 @dataclass,但当内存和性能需要达到极限时仍会考虑元组变种。

    • 不可与可变的切换:defaultdict、frozen 等参数的灵活组合让 @dataclass 天然能根据业务的稳定性要求调整可变性。

    • 警惕代码异味:如果数据类只承担存储功能而所有操作在外部完成,应当思考将部分行为迁移到数据类内部以避免维护陷阱。

    十一、章末思考问题

  • namedtuple 与 dataclass 的内存对比:创建一个 100 万条记录的列表,分别使用 namedtuple 和 @dataclass(slots=True) 存储相同数据,测量内存使用差异。slots=True(Python 3.10+)能为 @dataclass 提供更紧凑的内存结构。

  • 何时需要使用 default_factory=list:在 @dataclass 中,为什么不能直接使用 items: list = []?尝试移除 field 函数直接设置可变的默认值,观察异常现象。

  • frozen=True 与不可变性的本质:设置 frozen=True 只能阻止 dataclass 实例字段被再次赋值,但不能阻止包含的列表在内部被修改。思考如何实现“真正的深度不可变”。

  • 十二、下一章预告

    第 6 章《对象引用、可变性和垃圾回收》

    在学完如何简洁优雅地构建数据类之后,第 6 章将深入 Python 变量模型的本质:

    • 变量到底是什么:从“变量是盒子”转为“变量是便利贴”——Python 变量存储的是对象的引用,而非对象本身。这对理解赋值、函数参数传递、复制操作等有深远影响。
    • 标识(Identity)、相等性(Equality)和别名(Alias):is 和 == 的区别,以及什么时候应该使用哪一种。
    • 浅复制与深复制:使用 copy 模块的 copy() 和 deepcopy(),理解为什么某些情况下浅复制足以满足需求,某些情况必须深复制。
    • 垃圾回收与弱引用:Python 的垃圾回收基于引用计数,__del__ 方法在对象被回收前的最后执行。weakref 模块可以在不增加引用计数的情况下引用对象,在缓存等场景中不可或缺。
    • 参数传递机制:Python 唯一支持的参数传递模式是共享传参(call by sharing),因此理解函数内部是否可以修改传入的可变对象至关重要。
    • 可变对象的默认参数陷阱:函数参数默认值只计算一次,可变类型的默认参数会在函数调用之间共享状态。

    第 6 章将为后续理解第 8 章(对象引用)和第 21 章(弱引用与缓存)打下坚实基础,是从“能写代码”走向“写出健壮代码”的关键一环。


    本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
    👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

    赞(0)
    未经允许不得转载:171主机测评 » 《流畅的Python》读书笔记06: 第一部分 数据结构 - 数据类构建器
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址