在 Python 中,并没有一个叫做 struct 的内置数据类型(比如像 C 语言里的 struct),有几种非常主流和强大的方法来实现类似 struct 的功能,即把多个相关的整数(或其他数据)打包成一个单一的结构化对象。

最常用和最 Pythonic 的方法是使用 dataclasses 和 NamedTuple,对于需要高性能的场景,可以使用 ctypes 或 struct 模块(注意:这里的 struct 模块和 struct 数据类型是两回事)。
下面我将逐一介绍这些方法,并用一个统一的例子来对比它们。
示例场景
假设我们要表示一个“点”的坐标,它由 x 和 y 两个整数组成。
使用 dataclasses (推荐,现代 Python 的首选)
dataclasses 是 Python 3.7+ 引入的标准库,它用简洁的语法为类添加了数据存储、类型提示和常用方法(如 __repr__, __eq__),是创建结构化数据的首选方式。
代码示例
from dataclasses import dataclass
@dataclass
class Point:
"""使用 dataclass 定义一个结构化的点坐标"""
x: int
y: int
# 创建一个点的实例
p1 = Point(x=10, y=20)
# 访问属性
print(f"p1 的 x 坐标是: {p1.x}") # 输出: p1 的 x 坐标是: 10
print(f"p1 的 y 坐标是: {p1.y}") # 输出: p1 的 y 坐标是: 20
# 修改属性
p1.x = 100
print(f"修改后的 p1: {p1}") # 输出: 修改后的 p1: Point(x=100, y=20)
# 比较两个实例 (自动生成)
p2 = Point(x=10, y=20)
print(f"p1 和 p2 是否相等: {p1 == p2}") # 输出: p1 和 p2 是否相等: False (因为 p1.x 被改成了100)
p3 = Point(x=10, y=20)
print(f"p2 和 p3 是否相等: {p2 == p3}") # 输出: p2 和 p3 是否相等: True
优点
- 代码简洁:只需一个装饰器
@dataclass和类型注解。 - 可读性强:代码清晰明了,易于理解。
- 功能完整:自动生成
__init__,__repr__,__eq__等方法,减少了样板代码。 - 类型提示:支持静态类型检查,有助于 IDE 的代码补全和错误检测。
使用 typing.NamedTuple
NamedTuple 是 collections 模块的一部分(Python 3.3+ 后移至 typing),它创建的是元组的子类,既有元组的不可变性特点,又有类似类的可读性。
代码示例
from typing import NamedTuple
class Point(NamedTuple):
"""使用 NamedTuple 定义一个结构化的点坐标"""
x: int
y: int
# 创建一个实例 (方式像调用函数)
p1 = Point(x=10, y=20)
# 访问属性
print(f"p1 的 x 坐标是: {p1.x}") # 输出: p1 的 x 坐标是: 10
print(f"p1 的 y 坐标是: {p1.y}") # 输出: p1 的 y 坐标是: 20
# NamedTuple 是不可变的,不能修改属性
# p1.x = 100 # 这行代码会抛出 AttributeError: can't set attribute
# 比较两个实例 (基于值的比较)
p2 = Point(x=10, y=20)
print(f"p1 和 p2 是否相等: {p1 == p2}") # 输出: p1 和 p2 是否相等: True
优点
- 轻量级:比
dataclass更轻量,本质上是优化的元组。 - 不可变性:一旦创建,实例内容不能改变,这在某些场景下是优点(如作为字典的键)。
- 性能稍好:在属性访问和内存占用上可能比
dataclass稍有优势。
缺点
- 不可变:如果你需要修改实例的属性,
NamedTuple就不合适了。
使用 ctypes (与 C 语言交互)
ctypes 模块用于和 C 语言的数据类型进行交互,它可以直接创建内存中的 C 风格结构体,非常适合需要与 C 库交互或进行底层内存操作的场景。
代码示例
from ctypes import Structure, c_int
class Point(Structure):
"""使用 ctypes 定义一个与 C 兼容的结构体"""
_fields_ = [
("x", c_int),
("y", c_int)
]
# 创建实例
p1 = Point(x=10, y=20)
# 访问属性 (通过点号)
print(f"p1 的 x 坐标是: {p1.x}") # 输出: p1 的 x 坐标是: 10
print(f"p1 的 y 坐标是: {p1.y}") # 输出: p1 的 y 坐标是: 20
# 修改属性
p1.x = 100
print(f"修改后的 p1: {p1.x}, {p1.y}") # 输出: 修改后的 p1: 100, 20
# 获取内存地址
print(f"p1 在内存中的地址: {ctypes.addressof(p1)}")
优点
- C 语言兼容:可以直接传递给 C 函数,或用于解析二进制文件。
- 内存布局可控:可以精确控制结构体在内存中的对齐方式。
缺点
- 语法繁琐:需要定义
_fields_列表,不如前两种方法直观。 - 非 Pythonic:主要用于特定场景,不适合一般的应用程序开发。
使用 struct 模块 (打包/解包二进制数据)
struct 模块本身不是用来定义数据类型的,而是用来将 Python 的值(如整数)打包成二进制格式(bytes),或者从二进制数据中解包成 Python 值,它通常与文件 I/O 或网络通信结合使用。
虽然它不直接创建一个“结构体对象”,但它是实现结构化数据持久化和传输的核心工具。
代码示例
import struct
# 假设我们要打包一个点 (x=10, y=20)
# 'ii' 格式字符串表示两个 'int' 类型,每个 int 通常是 4 字节
# 字节顺序使用 '<' 表示小端法 (little-endian)
packed_data = struct.pack('<ii', 10, 20)
print(f"打包后的二进制数据: {packed_data}")
# 输出 (在 x86/x64 小端机器上): b'\n\x00\x00\x00\x14\x00\x00\x00'
# 解释: \n (10) + 3个字节0, \x14 (20) + 3个字节0
# 将二进制数据写入文件
with open('point.bin', 'wb') as f:
f.write(packed_data)
# 从文件中读取并解包
with open('point.bin', 'rb') as f:
# 读取 8 个字节 (因为两个 int 是 4*2=8 字节)
data_from_file = f.read(8)
x, y = struct.unpack('<ii', data_from_file)
print(f"从文件解包出的坐标: x={x}, y={y}") # 输出: 从文件解包出的坐标: x=10, y=20
优点
- 高效二进制处理:是处理二进制数据(如文件、网络包)的标准工具。
- 跨平台:可以指定字节顺序和对齐方式,确保数据在不同系统间正确传输。
缺点
- 不是数据类型:它是一个转换工具,不会创建一个可以在代码中直接使用的
Point对象。 - 不安全:如果二进制数据格式不匹配(如文件损坏),
unpack会抛出异常。
总结与对比
| 特性 | dataclasses |
typing.NamedTuple |
ctypes.Structure |
struct 模块 |
|---|---|---|---|---|
| 主要用途 | 创建 Python 内部的结构化对象 | 创建不可变的、类似元组的结构化对象 | 与 C 语言交互,底层内存操作 | 打包/解包二进制数据 |
| 可变性 | 可变 | 不可变 | 可变 | N/A (工具) |
| 语法 | @dataclass + 类型注解 |
class(NamedTuple) + 类型注解 |
class(Structure) + _fields_ |
struct.pack/unpack |
| Pythonic 程度 | 非常高 | 高 | 低 | 中 (作为工具) |
| 性能 | 良好 | 优秀 (轻量) | 接近 C | 非常快 (二进制操作) |
| 推荐场景 | 绝大多数 Python 应用 | 需要不可变数据时 (如字典键) | 调用 C 库、解析二进制文件 | 网络通信、文件 I/O |
最终建议
- 如果你想在 Python 代码中组织整数等数据:首选
dataclasses,它功能强大、语法简洁、易于维护,是现代 Python 开发的标准实践。 - 如果你的数据是不可变的,并且希望它像元组一样高效:使用
typing.NamedTuple。 - 如果你需要将数据写入文件、通过网络发送,或者与 C 语言库交互:使用
dataclasses或NamedTuple来定义你的数据模型,然后使用struct模块将其打包成二进制格式进行传输或存储。
