NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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:5711963

评论

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

从杀慢查询入手来预防 MySQL 雪崩的办法

Qunar技术沙龙

dba

有哪些好用的代码编辑器?

InfoQ IT百科

注册域名后,怎么创建个人网站?

InfoQ IT百科

TASKCTL 容器签出失败解决方法

TASKCTL

大数据 DevOps 分布式 ETL 自动化运维

TASKCTL 作业流程无触发设计

TASKCTL

大数据 DevOps 分布式 自动化部署 ETL任务

如何修改电脑文件格式?

InfoQ IT百科

主流的网站服务器架构有哪些?

InfoQ IT百科

现在常用的在线协作文档软件有哪些?

InfoQ IT百科

2022鲲鹏开发者创享日即将扬帆起航 与开发者共创未来共享非凡成就

科技热闻

如何添加字体?

InfoQ IT百科

怎么做SEO网站优化?

InfoQ IT百科

如何部署自己的网站?

InfoQ IT百科

Docker 实战教程之从入门到提高 (八)

Jerry Wang

Docker 容器 docker image 容器镜像 4月月更

移动App的设计流程是怎样的?

InfoQ IT百科

使用WPS需要注册/登录账号吗?

InfoQ IT百科

App怎么做灰度发布?

InfoQ IT百科

WPS有哪些隐藏的使用小技巧?

InfoQ IT百科

现在常用的视频会议软件有哪些?

InfoQ IT百科

目前WPS支持在哪些设备上使用?

InfoQ IT百科

如何优雅高效地管理公司文档?

小炮

文档 文档管理

怎么让网站在搜索结果中排更前?

InfoQ IT百科

WordPress 是什么?

InfoQ IT百科

App能收集哪些个人信息?

InfoQ IT百科

阿里云 云效一站式研发平台

阿里云云效

阿里云 DevOps 云原生 研发 一站式研发平台

Java篇-序列化与反序列化

是老郭啊

Java 对象 序列化 反序列化

一个WPS账号可以在多个设备同时登陆吗?

InfoQ IT百科

WPS是什么软件?

InfoQ IT百科

App分发是什么意思?

InfoQ IT百科

怎么做App分发?

InfoQ IT百科

有哪些适合程序员用的笔记应用?

InfoQ IT百科

有哪些比较靠谱的低代码开发平台?

InfoQ IT百科

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