写点什么

Python 中常见的数据结构:记录、结构体和纯数据对象

  • 2019-09-30
  • 本文字数:4645 字

    阅读完需:约 15 分钟

Python中常见的数据结构:记录、结构体和纯数据对象

与数组相比,记录数据结构中的字段数目固定,每个都有一个名称,类型也可以不同。


本文将介绍 Python 中的记录、结构体,以及“纯数据对象”,但只介绍标准库中含有的内置数据类型和类。


顺便说一句,这里的“记录”定义很宽泛。例如,这里也会介绍像 Python 的内置元组这样的类型。由于元组中的字段没有名称,因此一般不认为它是严格意义上的记录。


Python 提供了几种可用于实现记录、结构体和数据传输对象的数据类型。本节将快速介绍每个实现及各自特性,最后进行总结并给出一个决策指南,用来帮你做出自己的选择。


好吧,让我们开始吧!

字典——简单数据对象

Python 字典能存储任意数量的对象,每个对象都由唯一的键来标识。字典也常常称为映射或关联数组,能高效地根据给定的键查找、插入和删除所关联的对象。


Python 的字典还可以作为记录数据类型(record data type)或数据对象来使用。在 Python 中创建字典很容易,因为语言内置了创建字典的语法糖,简洁又方便。


字典创建的数据对象是可变的,同时由于可以随意添加和删除字段,因此对字段名称几乎没有保护措施。这些特性综合起来可能会引入令人惊讶的 bug,毕竟要在便利性和避免错误之间做出取舍。


car1 = {    'color': 'red',    'mileage': 3812.4,    'automatic': True,}car2 = {    'color': 'blue',    'mileage': 40231,    'automatic': False,}
# 字典有不错的__repr__方法:>>> car2{'color': 'blue', 'automatic': False, 'mileage': 40231}
# 获取mileage:>>> car2['mileage']40231
# 字典是可变的:>>> car2['mileage'] = 12>>> car2['windshield'] = 'broken'>>> car2{'windshield': 'broken', 'color': 'blue', 'automatic': False, 'mileage': 12}
# 对于提供错误、缺失和额外的字段名称并没有保护措施:car3 = { 'colr': 'green', 'automatic': False, 'windshield': 'broken',}
复制代码

元组——不可变对象集合

Python 元组是简单的数据结构,用于对任意对象进行分组。元组是不可变的,创建后无法修改。


在性能方面,元组占用的内存略少于 CPython 中的列表,构建速度也更快。


从如下反汇编的字节码中可以看到,构造元组常量只需要一个 LOAD_CONST 操作码,而构造具有相同内容的列表对象则需要多个操作:


>>> import dis>>> dis.dis(compile("(23, 'a', 'b', 'c')", '', 'eval'))      0 LOAD_CONST            4 ((23, 'a', 'b', 'c'))      3 RETURN_VALUE
>>> dis.dis(compile("[23, 'a', 'b', 'c']", '', 'eval')) 0 LOAD_CONST 0 (23) 3 LOAD_CONST 1 ('a') 6 LOAD_CONST 2 ('b') 9 LOAD_CONST 3 ('c') 12 BUILD_LIST 4 15 RETURN_VALUE
复制代码


不过你无须过分关注这些差异。在实践中这些性能差异通常可以忽略不计,试图通过用元组替换列表来获得额外的性能提升一般都是入了歧途。


单纯的元组有一个潜在缺点,即存储在其中的数据只能通过整数索引来访问,无法为元组中存储的单个属性制定一个名称,从而影响了代码的可读性。


此外,元组总是一个单例模式的结构,很难确保两个元组存储了相同数量的字段和相同的属性。


这样很容易因疏忽而犯错,比如弄错字段顺序。因此,建议尽可能减少元组中存储的字段数量。


# 字段:color、mileage、automatic>>> car1 = ('red', 3812.4, True)>>> car2 = ('blue', 40231.0, False)
# 元组的实例有不错的__repr__方法:>>> car1('red', 3812.4, True)>>> car2('blue', 40231.0, False)
# 获取mileage:>>> car2[1]40231.0
# 元组是可变的:>>> car2[1] = 12TypeError:"'tuple' object does not support item assignment"
# 对于错误或额外的字段,以及提供错误的字段顺序,并没有报错措施:>>> car3 = (3431.5, 'green', True, 'silver')
复制代码

编写自定义类——手动精细控制

类可用来为数据对象定义可重用的“蓝图”(blueprint),以确保每个对象都提供相同的字段。


普通的 Python 类可作为记录数据类型,但需要手动完成一些其他实现中已有的便利功能。例如,向__init__构造函数添加新字段就很烦琐且耗时。


此外,对于从自定义类实例化得到的对象,其默认的字符串表示形式没什么用。解决这个问题需要添加自己的__repr__方法。这个方法通常很冗长,每次添加新字段时都必须更新。


存储在类上的字段是可变的,并且可以随意添加新字段。使用 @property 装饰器能创建只读字段,并获得更多的访问控制,但是这又需要编写更多的胶水代码。


编写自定义类适合将业务逻辑和行为添加到记录对象中,但这意味着这些对象在技术上不再是普通的纯数据对象。


class Car:    def __init__(self, color, mileage, automatic):        self.color = color        self.mileage = mileage        self.automatic = automatic
>>> car1 = Car('red', 3812.4, True)>>> car2 = Car('blue', 40231.0, False)
# 获取mileage:>>> car2.mileage40231.0
# 类是可变的:>>> car2.mileage = 12>>> car2.windshield = 'broken'
# 类的默认字符串形式没多大用处,必须手动编写一个__repr__方法:>>> car1<Car object at 0x1081e69e8>
复制代码

collections.namedtuple——方便的数据对象

自 Python 2.6 以来添加的 namedtuple 类扩展了内置元组数据类型。与自定义类相似,namedtuple 可以为记录定义可重用的“蓝图”,以确保每次都使用正确的字段名称。


与普通的元组一样,namedtuple 是不可变的。这意味着在创建 namedtuple 实例之后就不能再添加新字段或修改现有字段。


除此之外,namedtuple 就相当于具有名称的元组。存储在其中的每个对象都可以通过唯一标识符访问。因此无须整数索引,也无须使用变通方法,比如将整数常量定义为索引的助记符。


namedtuple 对象在内部是作为普通的 Python 类实现的,其内存占用优于普通的类,和普通元组一样高效:


>>> from collections import namedtuple>>> from sys import getsizeof
>>> p1 = namedtuple('Point', 'x y z')(1, 2, 3)>>> p2 = (1, 2, 3)
>>> getsizeof(p1)72>>> getsizeof(p2)72
复制代码


由于使用 namedtuple 就必须更好地组织数据,因此无意中清理了代码并让其更加易读。


我发现从专用的数据类型(例如固定格式的字典)切换到 namedtuple 有助于更清楚地表达代码的意图。通常,每当我在用 namedtuple 重构应用时,都神奇地为代码中的问题想出了更好的解决办法。


用 namedtuple 替换普通(非结构化的)元组和字典还可以减轻同事的负担,因为用 namedtuple 传递的数据在某种程度上能做到“自说明”。


>>> from collections import namedtuple>>> Car = namedtuple('Car' , 'color mileage automatic')>>> car1 = Car('red', 3812.4, True)
# 实例有不错的__repr__方法:>>> car1Car(color='red', mileage=3812.4, automatic=True)
# 访问字段:>>> car1.mileage3812.4
# 字段是不可变的:>>> car1.mileage = 12AttributeError: "can't set attribute">>> car1.windshield = 'broken'AttributeError:"'Car' object has no attribute 'windshield'"
复制代码

typing.NamedTuple——改进版 namedtuple

这个类添加自 Python 3.6,是 collections 模块中 namedtuple 类的姊妹。它与 namedtuple 非常相似,主要区别在于用新语法来定义记录类型并支持类型注解(type hint)。


注意,只有像 mypy 这样独立的类型检查工具才会在意类型注解。不过即使没有工具支持,类型注解也可帮助其他程序员更好地理解代码(如果类型注解没有随代码及时更新则会带来混乱)。


>>> from typing import NamedTuple
class Car(NamedTuple): color: str mileage: float automatic: bool
>>> car1 = Car('red', 3812.4, True)
# 实例有不错的__repr__方法:>>> car1Car(color='red', mileage=3812.4, automatic=True)
# 访问字段:>>> car1.mileage3812.4
# 字段是不可变的:>>> car1.mileage = 12AttributeError: "can't set attribute">>> car1.windshield = 'broken'AttributeError:"'Car' object has no attribute 'windshield'"
# 只有像mypy 这样的类型检查工具才会落实类型注解:>>> Car('red', 'NOT_A_FLOAT', 99)Car(color='red', mileage='NOT_A_FLOAT', automatic=99)
复制代码

struct.Struct——序列化 C 结构体

struct.Struct 类用于在 Python 值和 C 结构体之间转换,并将其序列化为 Python 字节对象。例如可以用来处理存储在文件中或来自网络连接的二进制数据。


结构体使用与格式化字符串类似的语法来定义,能够定义并组织各种 C 数据类型(如 char、int、long,以及对应的无符号的变体)。


序列化结构体一般不用来表示只在 Python 代码中处理的数据对象,而是主要用作数据交换格式。


在某些情况下,与其他数据类型相比,将原始数据类型打包到结构体中占用的内存较少。但大多数情况下这都属于高级(且可能不必要的)优化。


>>> from struct import Struct>>> MyStruct = Struct('i?f')>>> data = MyStruct.pack(23, False, 42.0)
# 得到的是一团内存中的数据:>>> datab'\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00(B'
# 数据可以再次解包:>>> MyStruct.unpack(data)(23, False, 42.0)
复制代码

types.SimpleNamespace——花哨的属性访问

这里再介绍一种高深的方法来在 Python 中创建数据对象:types.SimpleNamespace。该类添加自 Python 3.3,可以用属性访问的方式访问其名称空间。


也就是说,SimpleNamespace 实例将其中的所有键都公开为类属性。因此访问属性时可以使用 obj.key 这样的点式语法,不需要用普通字典的 obj[‘key’]方括号索引语法。所有实例默认都包含一个不错的__repr__。


正如其名,SimpleNamespace 很简单,基本上就是扩展版的字典,能够很好地访问属性并以字符串打印出来,还能自由地添加、修改和删除属性。


>>> from types import SimpleNamespace>>> car1 = SimpleNamespace(color='red',...                        mileage=3812.4,...                        automatic=True)
# 默认的__repr__效果:>>> car1namespace(automatic=True, color='red', mileage=3812.4)
# 实例支持属性访问并且是可变的:>>> car1.mileage = 12>>> car1.windshield = 'broken'>>> del car1.automatic>>> car1namespace(color='red', mileage=12, windshield='broken')
复制代码

小结

那么在 Python 中应该使用哪种类型的数据对象呢?从上面可以看到,Python 中有许多不同的方法实现记录或数据对象,使用哪种方式通常取决于具体的情况。


如果只有两三个字段,字段顺序易于记忆或无须使用字段名称,则使用简单元组对象。例如三维空间中的(x, y, z)点。


如果需要实现含有不可变字段的数据对象,则使用 collections.namedtuple 或 typing.NamedTuple 这样的简单元组。


如果想锁定字段名称来避免输入错误,同样建议使用 collections.namedtuple 和 typing.NamedTuple。


如果希望保持简单,建议使用简单的字典对象,其语法方便,和 JSON 也类似。


如果需要对数据结构完全掌控,可以用 @property 加上设置方法和获取方法来编写自定义的类。


如果需要向对象添加行为(方法),则应该从头开始编写自定义类,或者通过扩展 collections.namedtuple 或 typing.NamedTuple 来编写自定义类。


如果想严格打包数据以将其序列化到磁盘上或通过网络发送,建议使用 struct.Struct。


一般情况下,如果想在 Python 中实现一个普通的记录、结构体或数据对象,我的建议是在{\rm Python}~2.x 中使用 collections.namedtuple,在 Python 3 中使用其姊妹 typing.NamedTuple。


本文内容来自作者图书作品《深入理解 Python 特性》,点击购买


2019-09-30 15:5713155

评论

发布
暂无评论
发现更多内容

Nginx 部署的虚拟主机如何使用 Let's Encrypt 来进行加密 https

HoneyMoose

ArrayBlockingQueue源码分析-构造方法

zarmnosaj

7月月更

Spring Cloud源码分析之Eureka篇第六章:服务注册

程序员欣宸

Java SpringCloud 7月月更

DelayQueue源码分析-特点与新增

zarmnosaj

7月月更

C语言入门(一)

逝缘~

c 7月月更

Redis stream性能测试实践【Java版】

FunTester

解读《深入理解计算机系统(CSAPP)》第5章优化程序性能

小明Java问道之路

优化逻辑 优化 优化技巧 程序优化 7月月更

Java多线程案例之线程池

未见花闻

7月月更

Python|「函数」递归与迭代

AXYZdong

Python 7月月更

Nginx Http模块是如何处理请求的?

Ethan

ngnix

spark调优(五):提交任务优化

怀瑾握瑜的嘉与嘉

7月月更

TSDB与Blockchain

CnosDB

IoT 时序数据库 开源社区 CnosDB infra

区块链技术带来司法“加速度”

CECBC

NumPy 与 Python 内置列表计算标准差的区别

宇宙之一粟

Numpy 7月月更

需求量最大的6个区块链工作

CECBC

【愚公系列】2022年7月 Go教学课程 007-计算机进制和变量命名规范

愚公搬代码

7月月更

初识Linkerd项目

阿泽🧸

Linkerd 7月月更

正则表达式(二)

Jason199

正则表达式 js 7月月更

2000字教你如何玩转Linux man命令,隐藏技能非常nice

wljslmz

Linux 运维 man 7月月更

iOS中方法和函数的区别

NewBoy

前端 移动端 iOS 知识体系

解读《深入理解计算机系统(CSAPP)》第4章处理器体系结构

小明Java问道之路

编译原理 编译器 指令集 7月月更 ISA

ORACLE进阶(十)start with connect by 实现递归查询

No Silver Bullet

oracle 递归 7月月更

跨域的问题终于能解决了

是乃德也是Ned

JavaScript ajax 前端 7月月更

自动生成API工具——Swagger3

Java学术趴

7月月更

应用性能管理与链路追踪的关系

穿过生命散发芬芳

链路追踪 7月月更

前端与HTML

小恺

7月月更

【刷题记录】4. 寻找两个正序数组的中位数

WangNing

7月月更

C++算法题中对于字符串的一些妙手

KEY.L

7月月更

鸿蒙 eTS 开发方式 Image 组件详解【续】

坚果

HarmonyOS OpenHarmony 7月月更

想要治好水,龙王也要拜拜这朵云

白洞计划

融云入选优秀厂商!|《2022中国信创生态市场研究及选型评估报告》(附下载)发布

融云 RongCloud

Python中常见的数据结构:记录、结构体和纯数据对象_编程语言_Dan Bader_InfoQ精选文章