Skip to content

Latest commit

 

History

History
346 lines (244 loc) · 14.8 KB

python-data-structures-compared.md

File metadata and controls

346 lines (244 loc) · 14.8 KB

Python 数据结构比较

原文:www.kdnuggets.com/2021/07/python-data-structures-compared.html

评论图示

图片由 Hitesh Choudhary 提供,来源于 Unsplash

选择存储数据的结构是解决编程任务和实施解决方案的重要部分,但往往没有得到应有的关注。不幸的是,在 Python 中,我经常看到列表被用作万能的数据结构。列表当然有其优点,但也有其缺点。还有很多其他的数据结构选项。


我们的前 3 个课程推荐

1. Google 网络安全证书 - 快速开启网络安全职业生涯。

2. Google 数据分析专业证书 - 提升你的数据分析技能

3. Google IT 支持专业证书 - 支持你的组织在 IT 方面


让我们来看看 5 种不同的 Python 数据结构,看看它们如何用于存储我们在日常任务中可能处理的数据,以及它们在存储时所用的相对内存和创建及访问时所需的时间。

数据结构的类型

首先,让我们列出我们将在此考虑的 5 种数据结构,并提供一些初步见解。

在这种情况下,我们讨论的是普通类(与下面的数据类相对),Python 文档对其进行了高层次的描述:

类提供了一种将数据和功能捆绑在一起的方法。创建一个新类会创建一种新的类型的对象,允许创建该类型的新实例。每个类实例可以附加属性以维护其状态。类实例也可以具有方法(由其类定义)以修改其状态。

使用类的优点在于它们是传统的,并且被广泛使用和理解。它们是否在相对需要的内存或时间上过于冗余,是需要考虑的问题。

数据类

在 Python 3.7 中新增的 数据类 是一种特殊的类,主要用于持有数据,提供了一些开箱即用的免费方法,用于典型功能,如实例化和打印实例内容。创建数据类是通过使用 @dataclass 装饰器来实现的。

尽管使用了非常不同的机制,数据类可以被认为是“带有默认值的可变命名元组”。因为数据类使用正常的类定义语法,你可以自由使用继承、 metaclasses、文档字符串、自定义方法、类工厂和其他 Python 类特性。这样的类被称为数据类,但实际上这个类没有什么特别之处:装饰器为类添加生成的方法,并返回与给定类相同的类。

正如你所见,自动生成的方法以及相关的时间节省是考虑数据类的主要原因。

命名元组

命名元组是优雅的实现一种有用的数据结构,本质上是带有命名字段的元组子类。

命名元组为元组中的每个位置赋予含义,使代码更具可读性和自文档性。它们可以在常规元组使用的地方使用,并且可以通过名称而不是位置索引访问字段。

初看,命名元组似乎是 Python 中最接近简单 C 风格结构体的东西,使得它们对许多人具有天然的吸引力。

字典

Python 字典是键值对的集合。

Python 字典是可变的无序集合(它们不记录元素位置或插入顺序)的键值对。字典中的键必须是唯一且可哈希的。这包括数字、字符串和元组等类型。列表和字典不能用作键,因为它们是可变的。

字典的优势在于它们简单,数据易于访问,而且使用和理解都很广泛。

列表

这就是所谓的“万用”Python 数据超结构,或者说很多代码会让你这么认为。实际上,列表是这样的:

列表是可变的有序且按索引排列的对象集合。列表的项是任意的 Python 对象。列表由放置在方括号中的逗号分隔的表达式列表组成。

为什么列表被广泛使用?它很简单易懂且易于实现,通常是学习 Python 时第一个接触的结构。是否存在与速度和内存使用相关的缺点?让我们来看看。

实现

首先,让我们看看这些结构的创建过程以及它们之间的比较。

我们可能使用这些数据结构存储数据的原因差异很大,但对于那些缺乏想象力的人,我们可以假设我们正在从 SQL 数据库中提取数据,需要将每条记录存储在这种结构中,以便在将数据进一步推进到我们的管道之前进行一些处理。

鉴于此,以下是创建每个五种结构的实例化代码。

""" class """
class CustomerClass:
    def __init__(self, cust_num:str, f_name:str, l_name:str, address:str,
                       city:str, state:str, phone:str, age:int):
        self.cust_num = cust_num
        self.f_name = f_name
        self.l_name = l_name
        self.address = address
        self.city = city
        self.state = state
        self.phone = phone
        self.age = age

    def to_string(self):
        return(f'{self.cust_num}, {self.f_name}, {self.l_name}, {self.age}, 
                 {self.address}, {self.city}, {self.state}, {self.phone}'stgrcutures)

""" data class """
from dataclasses import dataclass
@dataclass
class CustomerDataClass:
    cust_num: int
    f_name: str
    l_name: str
    address: str
    city: str
    state: str
    phone: str
    age: int

""" named tuple """
from collections import namedtuple
CustomerNamedTuple = namedtuple('CustomerNamedTuple', 
                                'cust_num f_name l_name address city state phone age')

""" dict """
def make_customer_dict(cust_num: int, f_name: str, l_name: str, address: str, 
                       city: str, state: str, phone: str, age: int):
    return {'cust_num': cust_num,
            'f_name': f_name,
            'l_name': l_name,
            'address': address,
            'city': city,
            'state': state,
            'phone': phone,
            'age': age}

""" list """
def make_customer_list(cust_num: int, f_name: str, l_name: str, address: str, 
                       city: str, state: str, phone: str, age: int):
    return [cust_num, f_name, l_name, address,
            city, state, phone, age]

注意以下几点:

  • 内置类型字典和列表的实例化已放入函数中。

  • 上述讨论中类和数据类实现之间的区别。

  • 命名元组的(显然主观的)优雅和简单性。

让我们看看这些结构的实例化情况,并比较实现这些操作所需的资源。

测试和结果。

我们将为每种结构创建一个单独的实例,每个实例中包含一个数据记录。我们将重复这个过程,将相同的数据字段用于每种结构 1,000,000 次,以获得更好的平均时间,并在我的一台简朴的 Dell 笔记本电脑上执行此过程,使用的是基于 Ubuntu 的操作系统。

Image

比较下面五种结构实例化的代码。

""" instantiating structures """

from sys import getsizeof
import time

# class
customer_1 = CustomerClass('EP90210', 'Edward', 'Perez', '123 Fake Street', 'Cityville', 'TX', '888-456-1234', 56)
print(f'Data: {customer_1.to_string()}')
print(f'Type: {type(customer_1)}')
print(f'Size: {getsizeof(customer_1)} bytes')

t0 = time.time()
for i in range(1000000):
    customer = CustomerClass('EP90210', 'Edward', 'Perez', '123 Fake Street', 'Cityville', 'TX', '888-456-1234', 56)
t1 = time.time()
print('Time: {:.3f}s\n'.format(t1-t0))

# data class
customer_2 = CustomerDataClass('EP90210', 'Edward', 'Perez', '123 Fake Street', 'Cityville', 'TX', '888-456-1234', 56)
print(f'Data: {customer_2}')
print(f'Type: {type(customer_2)}')
print(f'Size: {getsizeof(customer_2)} bytes')

t0 = time.time()
for i in range(1000000):
    customer = CustomerDataClass('EP90210', 'Edward', 'Perez', '123 Fake Street', 'Cityville', 'TX', '888-456-1234', 56)
t1 = time.time()
print('Time: {:.3f}s\n'.format(t1-t0))

# named tuple
customer_3 = CustomerNamedTuple('EP90210', 'Edward', 'Perez', '123 Fake Street', 'Cityville', 'TX', '888-456-1234', 56)
print(f'Data: {customer_3}')
print(f'Type: {type(customer_3)}')
print(f'Size: {getsizeof(customer_3)} bytes')

t0 = time.time()
for i in range(1000000):
    customer = CustomerNamedTuple('EP90210', 'Edward', 'Perez', '123 Fake Street', 'Cityville', 'TX', '888-456-1234', 56)
t1 = time.time()
print('Time: {:.3f}s\n'.format(t1-t0))

# dict
customer_4 = make_customer_dict('EP90210', 'Edward', 'Perez', '123 Fake Street', 'Cityville', 'TX', '888-456-1234', 56)
print(f'Data: {customer_4}')
print(f'Type: {type(customer_4)}')
print(f'Size: {getsizeof(customer_4)} bytes')

t0 = time.time()
for i in range(1000000):
    customer = make_customer_dict('EP90210', 'Edward', 'Perez', '123 Fake Street', 'Cityville', 'TX', '888-456-1234', 56)
t1 = time.time()
print('Time: {:.3f}s\n'.format(t1-t0))

# list
customer_5 = make_customer_list('EP90210', 'Edward', 'Perez', '123 Fake Street', 'Cityville', 'TX', '888-456-1234', 56)
print(f'Data: {customer_5}')
print(f'Type: {type(customer_5)}')
print(f'Size: {getsizeof(customer_5)} bytes')

t0 = time.time()
for i in range(1000000):
    customer = make_customer_list('EP90210', 'Edward', 'Perez', '123 Fake Street', 'Cityville', 'TX', '888-456-1234', 56)
t1 = time.time()
print('Time: {:.3f}s\n'.format(t1-t0))

这是上述内容的输出:

Data: EP90210, Edward, Perez, 56, 123 Fake Street, Cityville, TX, 888-456-1234
Type: <class '__main__.CustomerClass'>
Size: 56 bytes
Time: 0.657s

Data: CustomerDataClass(cust_num='EP90210', f_name='Edward', l_name='Perez', address='123 Fake Street', city='Cityville', state='TX', phone='888-456-1234', age=56)
Type: <class '__main__.CustomerDataClass'>
Size: 56 bytes
Time: 0.630s

Data: CustomerNamedTuple(cust_num='EP90210', f_name='Edward', l_name='Perez', address='123 Fake Street', city='Cityville', state='TX', phone='888-456-1234', age=56)
Type: <class '__main__.CustomerNamedTuple'>
Size: 112 bytes
Time: 0.447s

Data: {'cust_num': 'EP90210', 'f_name': 'Edward', 'l_name': 'Perez', 'address': '123 Fake Street', 'city': 'Cityville', 'state': 'TX', 'phone': '888-456-1234', 'age': 56}
Type: <class 'dict'>
Size: 368 bytes
Time: 0.318s

Data: ['EP90210', 'Edward', 'Perez', '123 Fake Street', 'Cityville', 'TX', '888-456-1234', 56]
Type: <class 'list'>
Size: 128 bytes
Time: 0.184s

最后,另一个有用的数据是了解我们结构中存储的值的相对访问时间(在下面的例子中为地址)。相同的检索操作将重复 1,000,000 次,下面报告了平均时间。

""" accessing an element """

# class
t0 = time.time()
for i in range(1000000):
    address = customer_1.address
t1 = time.time()
print(f'Type: {type(customer_1)}')
print('Time: {:.3f}s\n'.format(t1-t0))

# data class
t0 = time.time()
for i in range(1000000):
    address = customer_2.address
t1 = time.time()
print(f'Type: {type(customer_2)}')
print('Time: {:.3f}s\n'.format(t1-t0))

# named tuple
t0 = time.time()
for i in range(1000000):
    address = customer_3.address
t1 = time.time()
print(f'Type: {type(customer_3)}')
print('Time: {:.3f}s\n'.format(t1-t0))

# dictionary
t0 = time.time()
for i in range(1000000):
    address = customer_4['address']
t1 = time.time()
print(f'Type: {type(customer_4)}')
print('Time: {:.3f}s\n'.format(t1-t0))

# list
t0 = time.time()
for i in range(1000000):
    address = customer_5[3]
t1 = time.time()
print(f'Type: {type(customer_5)}')
print('Time: {:.3f}s\n'.format(t1-t0))

以及输出:

Type: <class '__main__.CustomerClass'>
Time: 0.098s

Type: <class '__main__.CustomerDataClass'>
Time: 0.092s

Type: <class '__main__.CustomerNamedTuple'>
Time: 0.134s

Type: <class 'dict'>
Time: 0.095s

Type: <class 'list'>
Time: 0.117s

本文的意图不是推荐使用哪种数据结构,也不是建议存在一个适用于所有情况的最佳结构。相反,我们想要了解一些不同的选项及其相对的优缺点。像所有事情一样,必须做出权衡,并且在做出这些决策时,诸如可理解性、易用性等定量考虑因素也需要纳入考虑。

也就是说,从以上分析中有一些事情显而易见:

  1. 在所有结构中,字典使用的存储量最大,在我们的例子中几乎是下一个最大结构的 3 倍——不过在我们查看扩展效应和内部字段数据类型之前,应该谨慎做出概括。

  2. 不出所料,列表的实例化速度最快,但从中检索元素的速度却不是最快(几乎是最慢的)。

  3. 在我们的例子中,命名元组是检索元素时速度最慢的结构,但在存储空间方面居中。

  4. 两个类的实例化相对较长(这是预期的),但在元素检索和空间使用方面,两者与其他结构相比都非常具有竞争力。

所以我们不仅不打算在每种情况下推荐单一结构,也没有明确的赢家可以在每种情况下推荐。即使基于我们的小实验谨慎地进行一般化,显然在选择用于特定工作的结构时需要考虑优先级。至少,这些有限的实验提供了一些对 Python 中数据结构性能的小窗口洞察。

相关

  • 作为数据科学家管理你的可重用 Python 代码

  • 5 个 Python 数据处理技巧及代码片段

  • Python 中的日期处理和特征工程

更多相关内容