漫谈 Python 中的数据结构 (2)—记录、结构体和数据传输对象
记录、结构体和数据传输对象
与数组相比,记录数据结构提供了一种相对固定的数据结构,记录通常是由多个字段所组成,每个字段都有自己的名字,也可以有不同的类型。
在这一节中,将看到如何在 Python 中,只使用标准库中的内置数据类型和类来实现记录、结构和普通的数据对象。
注意:在这里,将比较宽松地使用了记录的定义。例如,我还将讨论像 Python 内置的 tuple 这样的类型,这些类型在严格意义上可能被认为是记录,也可能不是,因为不提供命名的字段
Python 提供了几种数据类型,你可以用来实现记录、结构和数据传输对象这些数据结构。在这里,将快速了解每个实现及其独特的特征。接下来还会给出一些建议帮助做出自己的选择。
dict:简单的数据对象
如前所述,Python 中的字典存储了任意数量的对象,每个对象由一个唯一的键来标识。字典也经常被称为 map 或关联数组,在 dict 中我们可以通过键来快速地查找、插入和删除任何对象。
在 Python 中,使用字典记录数据类型或数据对象是非常方便的。在 Python 中,创建一个字典数据结构很容易创建,这是因为在 python 中,有自己的语法糖。
使用字典创建的数据对象是可变的,因为字段可以是自由地添加和删除,所以在字典中,对于拼错的字段名这样错误并没有什么好的措施来应对,这是字典优点同时也是字典缺点,所以便利性同时也势必带来易错难于维护的问题。
tut1 = {
"title":"machinelearning",
"price":1999.9,
"isFinished":True
}
tuple 不可变的对象组
在 Python 中,tuple 是一种的数据结构,在其他语言中,我们也见过这样数据结构,用于分组任意的对象。tuple 是不可变的--它们一旦被创建就不能被修改。
从性能上讲,tuple 要比 CPython 中的 list 占用的内存更少,创建的速度也更快。
接下来我们从字节码层面上来看一下为什么说创建 tuple 要比 list 更快呢? 构造 tuple 常量时,只需要 LOAD_CONST 操作码,而构造具有相同内容的 list 对象则需要更多的操作。
import dis
dis.dis(compile("(12,'a','b','c')","","eval"))
1 0 LOAD_CONST 0 ((12, 'a', 'b', 'c'))
2 RETURN_VALUE
dis.dis(compile("[12,'a','b','c']","","eval"))
1 0 LOAD_CONST 0 (12)
2 LOAD_CONST 1 ('a')
4 LOAD_CONST 2 ('b')
6 LOAD_CONST 3 ('c')
8 BUILD_LIST 4
10 RETURN_VALUE
然而,在实际开发过程,往往会忽略性能这点差异,仅从性能上,就使用 tuple 来代替 list 可能并不算明智的做法。
在 tuple 中的一个潜在缺点是,存储在其中的数据只能通过整数索引来访问 tuple 中元素。不能给存储在 tuple 中的各个元素指定名字,因此影响了代码的可读性。
tuple 总是一个临时的结构,很难保证两个 tuple 有相同数量的字段和相同的属性存储在其中。因此很容易疏忽而写错误
tut1 = ("machinelearning",1999.9,True)
自定义的类: 更多的工作,更多的控制
如果以上都无法满足你对数据结构描述,可以自己定义类来记录数据。
使用常规的 Python 的类作为记录数据类型是可行的,但也自己动手来一个类。例如,在 __init__
构造函数中添加新的字段,这样做虽然代码可读性大大提高,不过也需要时间。
另外,对于从自定义类,到实例化的对象,默认的字符串表示法可能还需要自己动手来丰衣足食,自己去写 __repr__
方法,而且我们还需要维护这个方法,每次新增字段还都必须更新一些 __repr__
这个方法。
存储在类上的字段是可变的,字段可以被自由添加,关于这个点,对于你来说可能喜欢也可能不喜欢。有可能提供更多的访问控制,并使用 @property
装饰器创建只读字段,但这同样需要编写更多的胶水代码。
如果需要对记录添加更多业务逻辑和行为,那么编写自定义类是一个不错的选择。
class Tut:
def __init__(self,title,price,isFinished=True):
self.title = title
self.price = price
self.isFinished = isFinished
dataclasses.dataclass。Python 3.7 以上的数据类
dataclass 是在 Python 3.7 及以上版本中可用,如果想用类来存储数据 dataclass 是一个比较不错选择。dataclass 不同于普通的 Python 类,是为储存数据结构
通过编写一个数据类而不是普通的 Python 类,你的对象实例得到了一些开箱即用的有用特性,这将为你节省一些打字和手工实现的工作。
- 定义实例变量的语法更短,因为你不需要实现
.__init__()
方法。 - 数据类的实例中,会自动生成的
.__repr__()
方法自动获得不错的字符串表示,不必自己手动来写 - 实例变量接受类型注解,使你的数据类在一定程度上实现了自我记录。请记住,类型注释只是一种提示,如果没有单独的类型检查工具,是不会被强制执行的。
数据类通常使用 @dataclass
装饰器来创建,正如你在下面的代码例子中看到的那样。
from dataclasses import dataclass
@dataclass
class Tut:
title:str
price:float
isFinished:bool
collections.namedtuple: 方便的数据对象
在 Python 2.6 中,提供了 namedtuple 类,是对内置 tuple 数据类型的扩展。类似于定义一个自定义的类,使用 namedtuple 可以记录定义可重用的蓝图,确保使用正确的字段名。
namedtuple 对象是不可变的,就像普通的 tuple 一样。这意味着在 namedtuple 实例化后,就不允许添加新字段或修改现有字段。
除此之外,named tuple 对象是,每个存储在其中的对象都可以通过一个唯一的标识符来访问。这使你不必记住整数索引,定义整数常数作为索引的助记符。
namedtuple 对象在内部实现为普通的 Python 类。不过在内存的占用时,却要比普通的类占用内存更少,和 tuple 一样节省内存。
p1 = namedtuple("Point","x y z")(1,2,3)
p2 = (1,2,3)
print(getsizeof(p1))
print(getsizeof(p2))
namedtuple 对象可以让你的代码的看起来更整洁,通过为数据添加一些结构化信息增加了数据的可读性。
我发现 namedtuple 这样固定格式的字典等,临时数据类型到 namedtuple 对象可以帮助我们更清楚地表达代码的意图。通常情况下,在这种重构时。
普通的(非结构化的) tuple 和 namedtuple 对象,也可以使你的同事的生活更轻松,因为它使正在传递的数据自我记录,至少在某种程度上。
from collections import namedtuple
Employee = namedtuple("Employee","name age salary")
employee= Employee("mike",28,2999.9)
print(employee)
employee.name = "tony"
AttributeError: can't set attribute
typing.NamedTuple: 改进后的 tuple
typing.NamedTuple
是在 python3.6 中添加的,是模块 namedtuple 的小兄弟,与 namedtuple 非常类似,主要区别在于更新,主要区别可能就是语法上,新增了对类型提示的支持
不过值得注意,如果没有安装独立的类型检查工具(如mypy),类型注释检测工作是不会被执行的,而被忽略。但即使没有工具的支持,这些额外的信息也可以为其他程序员提供有用的提示,在大型项目过程显得特别有效。
from typing import NamedTuple
class Employee(NamedTuple):
name:str
age:int
salary:float
employee_1 = Employee("mike",28,15000.0)
print(employee_1)
struct.结构:序列化 C 结构
struct.Struct 类在 Python 值和序列化为 Python 字节对象的 C 结构之间进行转换。例如,可以用来处理存储在文件中或来自网络连接的二进制数据。
Structs 是用一种基于格式化字符串的语言来定义的,允许你定义各种 C 数据类型的排列,如 char、int 和 long 以及它们的无符号变体。
序列化的结构很少用来表示在 Python 代码中处理的数据对象。主要是作为一种数据交换格式,而不是作为在内存中保存仅由 Python 代码使用的数据的一种方式。
在某些情况下,将原始数据打包到结构中可能比将其保存在其他数据类型中,使用的内存会更少。然而,在大多数情况下,这将是比较高级的(而且很少用到)优化方式。
- 按照指定格式将Python数据转换为字符串,该字符串为字节流,可以用于在文件或者数据库存在字节码,如网络传输时,不能传输int,此时先将int转化为字节流,然后再发送
from struct import Struct
MyStruct = Struct("i?f")
data = MyStruct.pack(1,True,15000.0)
print(data)
print(MyStruct.unpack(data))
?
: 表示布尔类型h
: shortl
: longi
: intf
: floatq
: long long int
提供一些方法
struct.pack()
data = struct.pack('hhl',5,10,15)
print(data)
print(struct.unpack('hhl',data))
types.SimpleNamespace: 华丽的属性访问
这里还有一个在 Python 中, types.SimpleNamespace 这个类是在 Python 3.3 中添加的,提供了对其命名空间的属性访问。
这意味着 SimpleNamespace 实例将所有的键作为类的属性。你可以使用 obj.key
方式来访问属性,而不是普通 dicts 使用的 obj['key']
方括号的方式来访问属性。所有的实例默认都包括一个有意义的 __repr__
。
正如 SimpleNamespace 字一样,这是一个很简单 基本上是一个允许属性访问并能很好地打印的字典。可以自由第添加、修改和删除属性。
from types import SimpleNamespace
employee_1 = SimpleNamespace(name="mike",age=28,salary=15000.0)
print(employee_1)
print(employee_1.name)
简单总结一下 上面介绍几种可以用来表示记录的数据结构,各自有自己特点,接下来我们简单总结一下,看看在什么场景下应该使用哪一种数据结构
- 当数据只有几个相对固定的字段,那么如果字段顺序容易记忆或者字段名是非必要,例如 三维空间中的一个 (x,y,z) 点,使用 tuple 就可以
- 需要不可变的字段,那么普通的 tuple 、collections.namedtuple 和 typing.NamedTuple 也可以做后备
- 如果避免字段名写错也可以选择 collections.namedtuple 和 typing.NamedTuple
- 想保持简单,其实普通的字典对象可能是一个不错的选择,因为字典语法相对简单,且其格式 JSON 非常相似,当转换 json 可以优先考虑字典
- 想要对数据结构有更多控制,那么就可以考虑自己定义个类
- 需要向对象添加行为(方法),也可以考虑自定义类,也可以考虑使用 dataclass 装饰器来写数据类,或者通过扩展 collections.namedtuple 或 typing.NamedTuple
- 需要将数据进行压缩打包以序列化保存在磁盘或在网络上发送,就可以看一看 struct.Struct 了
参考文献
转载自:https://juejin.cn/post/7171063704249696286