likes
comments
collection
share

漫谈 Python 中的数据结构 (2)—记录、结构体和数据传输对象

作者站长头像
站长
· 阅读数 74

记录、结构体和数据传输对象

与数组相比,记录数据结构提供了一种相对固定的数据结构,记录通常是由多个字段所组成,每个字段都有自己的名字,也可以有不同的类型。

在这一节中,将看到如何在 Python 中,只使用标准库中的内置数据类型和类来实现记录、结构和普通的数据对象。

注意:在这里,将比较宽松地使用了记录的定义。例如,我还将讨论像 Python 内置的 tuple 这样的类型,这些类型在严格意义上可能被认为是记录,也可能不是,因为不提供命名的字段

Python 提供了几种数据类型,你可以用来实现记录、结构和数据传输对象这些数据结构。在这里,将快速了解每个实现及其独特的特征。接下来还会给出一些建议帮助做出自己的选择。

漫谈 Python 中的数据结构 (2)—记录、结构体和数据传输对象

dict:简单的数据对象

如前所述,Python 中的字典存储了任意数量的对象,每个对象由一个唯一的键来标识。字典也经常被称为 map 或关联数组,在 dict 中我们可以通过键来快速地查找、插入和删除任何对象。

在 Python 中,使用字典记录数据类型或数据对象是非常方便的。在 Python 中,创建一个字典数据结构很容易创建,这是因为在 python 中,有自己的语法糖。

使用字典创建的数据对象是可变的,因为字段可以是自由地添加和删除,所以在字典中,对于拼错的字段名这样错误并没有什么好的措施来应对,这是字典优点同时也是字典缺点,所以便利性同时也势必带来易错难于维护的问题。

tut1 = {
   "title":"machinelearning",
   "price":1999.9,
   "isFinished":True 
}

漫谈 Python 中的数据结构 (2)—记录、结构体和数据传输对象

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__ 构造函数中添加新的字段,这样做虽然代码可读性大大提高,不过也需要时间。

漫谈 Python 中的数据结构 (2)—记录、结构体和数据传输对象

另外,对于从自定义类,到实例化的对象,默认的字符串表示法可能还需要自己动手来丰衣足食,自己去写 __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 对象,也可以使你的同事的生活更轻松,因为它使正在传递的数据自我记录,至少在某种程度上。

漫谈 Python 中的数据结构 (2)—记录、结构体和数据传输对象

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),类型注释检测工作是不会被执行的,而被忽略。但即使没有工具的支持,这些额外的信息也可以为其他程序员提供有用的提示,在大型项目过程显得特别有效。

漫谈 Python 中的数据结构 (2)—记录、结构体和数据传输对象

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: short
  • l: long
  • i: int
  • f: float
  • q: 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 了

参考文献

参考 Common Python Data Structures(Guide)

转载自:https://juejin.cn/post/7171063704249696286
评论
请登录