记录的不同表示方式

PythonPythonBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在这个实验中,你将学习在 Python 中探索存储大型数据集的内存高效方法。你还将发现表示记录的不同方式,例如元组(tuples)、字典(dictionaries)、类(classes)和命名元组(named tuples)。

此外,你将比较不同数据结构的内存使用情况。理解这些结构之间的权衡对于进行数据分析的 Python 用户来说非常有价值,因为这有助于优化代码。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/DataStructuresGroup(["Data Structures"]) python(("Python")) -.-> python/FileHandlingGroup(["File Handling"]) python(("Python")) -.-> python/PythonStandardLibraryGroup(["Python Standard Library"]) python/DataStructuresGroup -.-> python/tuples("Tuples") python/DataStructuresGroup -.-> python/dictionaries("Dictionaries") python/FileHandlingGroup -.-> python/file_opening_closing("Opening and Closing Files") python/FileHandlingGroup -.-> python/file_reading_writing("Reading and Writing Files") python/FileHandlingGroup -.-> python/file_operations("File Operations") python/FileHandlingGroup -.-> python/with_statement("Using with Statement") python/PythonStandardLibraryGroup -.-> python/data_collections("Data Collections") subgraph Lab Skills python/tuples -.-> lab-132428{{"记录的不同表示方式"}} python/dictionaries -.-> lab-132428{{"记录的不同表示方式"}} python/file_opening_closing -.-> lab-132428{{"记录的不同表示方式"}} python/file_reading_writing -.-> lab-132428{{"记录的不同表示方式"}} python/file_operations -.-> lab-132428{{"记录的不同表示方式"}} python/with_statement -.-> lab-132428{{"记录的不同表示方式"}} python/data_collections -.-> lab-132428{{"记录的不同表示方式"}} end

探索数据集

让我们从仔细研究将要使用的数据集开始我们的旅程。文件 ctabus.csv 是一个 CSV(逗号分隔值)文件。CSV 文件是存储表格数据的常用方法,其中每一行代表一个行,行内的值用逗号分隔。这个特定的文件包含芝加哥交通管理局(Chicago Transit Authority, CTA)公交系统的每日乘客量数据,涵盖从 2001 年 1 月 1 日到 2013 年 8 月 31 日的期间。

解压文件并删除 zip 文件:

cd /home/labex/project
unzip ctabus.csv.zip
rm ctabus.csv.zip

为了理解这个文件的结构,我们首先要查看它的内部。我们将使用 Python 读取文件并打印一些行。打开终端并运行以下 Python 代码:

f = open('/home/labex/project/ctabus.csv')
print(next(f))  ## 读取标题行
print(next(f))  ## 读取第一条数据行
print(next(f))  ## 读取第二条数据行
f.close()

在这段代码中,我们首先使用 open 函数打开文件,并将其赋值给变量 fnext 函数用于从文件中读取下一行。我们使用它三次:第一次读取标题行,标题行通常包含数据集中列的名称。第二次和第三次,我们分别读取第一条和第二条数据行。最后,我们使用 close 方法关闭文件,以释放系统资源。

你应该看到类似这样的输出:

route,date,daytype,rides

3,01/01/2001,U,7354

4,01/01/2001,U,9288

此输出显示该文件有 4 列数据。让我们分解一下每一列代表什么:

  1. route:这是公交线路名称或号码。它是数据集中的第一列(第 0 列)。
  2. date:这是一个日期字符串,格式为 MM/DD/YYYY。这是第二列(第 1 列)。
  3. daytype:这是一个日期类型代码,它是第三列(第 2 列)。
    • U = 星期日/假日
    • A = 星期六
    • W = 工作日
  4. rides:此列记录了总乘客数,为一个整数。它是第四列(第 3 列)。

rides 列告诉我们,在特定日期,特定线路上有多少人乘坐了公交车。例如,从上面的输出中,我们可以看到 2001 年 1 月 1 日有 7354 人乘坐了 3 路公交车。

现在,让我们找出文件中包含多少行。了解行数可以让我们了解数据集的大小。运行以下 Python 代码:

with open('/home/labex/project/ctabus.csv') as f:
    line_count = sum(1 for line in f)
    print(f"Total lines in the file: {line_count}")

在这段代码中,我们使用 with 语句打开文件。使用 with 的优点是,它会在我们完成文件操作后自动关闭文件。然后,我们使用生成器表达式 (1 for line in f) 创建一个 1 的序列,文件中每一行对应一个 1。 sum 函数将所有这些 1 相加,从而得出文件中的总行数。最后,我们打印出结果。

这应该输出大约 577564 行,这意味着我们正在处理一个相当大的数据集。这个大型数据集将为我们提供大量数据,以供分析和从中获得见解。

使用不同存储方法测量内存使用情况

在这一步中,我们将探讨不同的数据存储方式如何影响内存使用。内存使用是编程中的一个重要方面,尤其是在处理大型数据集时。为了测量 Python 代码的内存使用情况,我们将使用 Python 的 tracemalloc 模块。这个模块非常有用,因为它允许我们跟踪 Python 进行的内存分配。通过使用它,我们可以了解数据存储方法消耗了多少内存。

方法 1:将整个文件存储为单个字符串

让我们先创建一个新的 Python 文件。导航到 /home/labex/project 目录并创建一个名为 memory_test1.py 的文件。你可以使用文本编辑器打开这个文件。文件打开后,添加以下代码。这段代码将把文件的整个内容作为单个字符串读取,并测量内存使用情况。

## memory_test1.py
import tracemalloc

def test_single_string():
    ## 开始跟踪内存
    tracemalloc.start()

    ## 将整个文件作为单个字符串读取
    with open('/home/labex/project/ctabus.csv') as f:
        data = f.read()

    ## 获取内存使用统计信息
    current, peak = tracemalloc.get_traced_memory()

    print(f"File length: {len(data)} characters")
    print(f"Current memory usage: {current/1024/1024:.2f} MB")
    print(f"Peak memory usage: {peak/1024/1024:.2f} MB")

    ## 停止跟踪内存
    tracemalloc.stop()

if __name__ == "__main__":
    test_single_string()

添加代码后,保存文件。现在,要运行这个脚本,打开终端并执行以下命令:

python3 /home/labex/project/memory_test1.py

运行脚本时,你应该会看到类似以下的输出:

File length: 12361039 characters
Current memory usage: 11.80 MB
Peak memory usage: 23.58 MB

具体的数字在你的系统上可能会有所不同,但通常你会注意到当前内存使用量约为 12 MB,峰值内存使用量约为 24 MB。

方法 2:存储为字符串列表

接下来,我们将测试另一种存储数据的方法。在同一个 /home/labex/project 目录下创建一个名为 memory_test2.py 的新文件。在编辑器中打开这个文件并添加以下代码。这段代码读取文件并将每行作为一个单独的字符串存储在列表中,然后测量内存使用情况。

## memory_test2.py
import tracemalloc

def test_list_of_strings():
    ## 开始跟踪内存
    tracemalloc.start()

    ## 将文件读取为字符串列表(每行一个字符串)
    with open('/home/labex/project/ctabus.csv') as f:
        lines = f.readlines()

    ## 获取内存使用统计信息
    current, peak = tracemalloc.get_traced_memory()

    print(f"Number of lines: {len(lines)}")
    print(f"Current memory usage: {current/1024/1024:.2f} MB")
    print(f"Peak memory usage: {peak/1024/1024:.2f} MB")

    ## 停止跟踪内存
    tracemalloc.stop()

if __name__ == "__main__":
    test_list_of_strings()

保存文件,然后在终端中使用以下命令运行脚本:

python3 /home/labex/project/memory_test2.py

你应该会看到类似以下的输出:

Number of lines: 577564
Current memory usage: 43.70 MB
Peak memory usage: 43.74 MB

注意,与之前将数据存储为单个字符串的方法相比,内存使用量显著增加。这是因为列表中的每行都是一个单独的 Python 字符串对象,每个对象都有自己的内存开销。

理解内存差异

这两种方法在内存使用上的差异展示了 Python 编程中一个重要的概念,即对象开销。当你将数据存储为字符串列表时,每个字符串都是一个单独的 Python 对象。每个对象都有一些额外的内存需求,包括:

  1. Python 对象头(通常每个对象 16 - 24 字节)。这个头包含了对象的相关信息,如类型和引用计数。
  2. 实际的字符串表示本身,用于存储字符串的字符。
  3. 内存对齐填充。这是为了确保对象的内存地址能够被有效访问而添加的额外空间。

另一方面,当你将整个文件内容存储为单个字符串时,只有一个对象,因此只有一组开销。考虑到数据的总大小,这种方式在内存使用上更高效。

在设计处理大型数据集的程序时,你需要考虑内存效率和数据可访问性之间的权衡。有时,将数据存储为字符串列表可能更便于访问,但会使用更多的内存。而在其他时候,你可能会优先考虑内存效率,选择将数据存储为单个字符串。

使用元组处理结构化数据

到目前为止,我们一直在处理原始文本数据的存储。但在进行数据分析时,我们通常需要将数据转换为更有条理和结构化的格式。这样可以更轻松地执行各种操作,并从数据中获取有价值的信息。在这一步中,我们将学习如何使用 csv 模块将数据读取为元组列表。元组是 Python 中一种简单而实用的数据结构,可以容纳多个值。

创建使用元组的读取函数

让我们在 /home/labex/project 目录下创建一个名为 readrides.py 的新文件。这个文件将包含从 CSV 文件中读取数据并将其存储为元组列表的代码。

## readrides.py
import csv
import tracemalloc

def read_rides_as_tuples(filename):
    '''
    以元组列表的形式读取公交出行数据
    '''
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        headings = next(rows)     ## 跳过标题行
        for row in rows:
            route = row[0]
            date = row[1]
            daytype = row[2]
            rides = int(row[3])
            record = (route, date, daytype, rides)
            records.append(record)
    return records

if __name__ == '__main__':
    tracemalloc.start()

    rows = read_rides_as_tuples('/home/labex/project/ctabus.csv')

    current, peak = tracemalloc.get_traced_memory()
    print(f'Number of records: {len(rows)}')
    print(f'First record: {rows[0]}')
    print(f'Second record: {rows[1]}')
    print(f'Memory Use: Current {current/1024/1024:.2f} MB, Peak {peak/1024/1024:.2f} MB')

这个脚本定义了一个名为 read_rides_as_tuples 的函数。下面是它的具体步骤:

  1. 打开由 filename 参数指定的 CSV 文件,这样我们就可以访问文件中的数据。
  2. 使用 csv 模块解析文件的每一行。csv.reader 函数帮助我们将每行拆分为单个值。
  3. 从每行中提取四个字段(线路、日期、日期类型和乘客数量)。这些字段对我们的数据分析很重要。
  4. rides 字段转换为整数。这是必要的,因为 CSV 文件中的数据最初是字符串格式,而我们需要一个数值进行计算。
  5. 创建一个包含这四个值的元组。元组是不可变的,这意味着一旦创建,它们的值就不能更改。
  6. 将元组添加到名为 records 的列表中。这个列表将包含 CSV 文件中的所有记录。

现在,让我们运行这个脚本。打开终端并输入以下命令:

python3 /home/labex/project/readrides.py

你应该会看到类似以下的输出:

Number of records: 577563
First record: ('3', '01/01/2001', 'U', 7354)
Second record: ('4', '01/01/2001', 'U', 9288)
Memory Use: Current 89.12 MB, Peak 89.15 MB

注意,与我们之前的示例相比,内存使用量增加了。这有几个原因:

  1. 我们现在以结构化格式(元组)存储数据。结构化数据通常需要更多内存,因为它有明确的组织方式。
  2. 元组中的每个值都是一个单独的 Python 对象。Python 对象有一定的开销,这导致了内存使用量的增加。
  3. 我们有一个额外的列表结构来保存所有这些元组。列表也会占用内存来存储其元素。

使用这种方法的优点是,我们的数据现在已经正确结构化,并且可以进行分析。我们可以通过索引轻松访问每条记录的特定字段。例如:

## 访问元组元素的示例(将此代码添加到 readrides.py 文件中进行尝试)
first_record = rows[0]
route = first_record[0]
date = first_record[1]
daytype = first_record[2]
rides = first_record[3]
print(f"Route: {route}, Date: {date}, Day type: {daytype}, Rides: {rides}")

然而,通过数字索引访问数据并不总是直观的。尤其是在处理大量字段时,很难记住哪个索引对应哪个字段。在下一步中,我们将探索其他数据结构,使我们的代码更具可读性和可维护性。

✨ 查看解决方案并练习

比较不同的数据结构

在 Python 中,数据结构用于组织和存储相关数据。它们就像容器,以结构化的方式容纳不同类型的信息。在这一步中,我们将比较不同的数据结构,并了解它们的内存使用情况。

让我们在 /home/labex/project 目录下创建一个名为 compare_structures.py 的新文件。这个文件将包含从 CSV 文件中读取数据并将其存储在不同数据结构中的代码。

## compare_structures.py
import csv
import tracemalloc
from collections import namedtuple

## 为出行数据定义一个命名元组
RideRecord = namedtuple('RideRecord', ['route', 'date', 'daytype', 'rides'])

## 命名元组是一种轻量级类,允许你通过名称访问其字段。
## 它类似于元组,但具有命名属性。

## 定义一个使用 __slots__ 进行内存优化的类
class SlottedRideRecord:
    __slots__ = ['route', 'date', 'daytype', 'rides']

    def __init__(self, route, date, daytype, rides):
        self.route = route
        self.date = date
        self.daytype = daytype
        self.rides = rides

## 使用 __slots__ 的类是一种内存优化类。
## 它避免使用实例字典,从而节省内存。

## 为出行数据定义一个普通类
class RegularRideRecord:
    def __init__(self, route, date, daytype, rides):
        self.route = route
        self.date = date
        self.daytype = daytype
        self.rides = rides

## 普通类是一种面向对象的数据表示方式。
## 它具有命名属性,并且可以有方法。

## 以元组形式读取数据的函数
def read_as_tuples(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## 跳过标题行
        for row in rows:
            record = (row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## 此函数从 CSV 文件中读取数据并将其存储为元组。
## 元组是不可变序列,你可以通过数字索引访问其元素。

## 以字典形式读取数据的函数
def read_as_dicts(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        headers = next(rows)  ## 获取标题行
        for row in rows:
            record = {
                'route': row[0],
                'date': row[1],
                'daytype': row[2],
                'rides': int(row[3])
            }
            records.append(record)
    return records

## 此函数从 CSV 文件中读取数据并将其存储为字典。
## 字典使用键值对,因此你可以通过名称访问元素。

## 以命名元组形式读取数据的函数
def read_as_named_tuples(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## 跳过标题行
        for row in rows:
            record = RideRecord(row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## 此函数从 CSV 文件中读取数据并将其存储为命名元组。
## 命名元组结合了元组的效率和命名访问的可读性。

## 以普通类实例形式读取数据的函数
def read_as_regular_classes(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## 跳过标题行
        for row in rows:
            record = RegularRideRecord(row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## 此函数从 CSV 文件中读取数据并将其存储为普通类的实例。
## 普通类允许你为数据添加方法。

## 以使用 __slots__ 的类实例形式读取数据的函数
def read_as_slotted_classes(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## 跳过标题行
        for row in rows:
            record = SlottedRideRecord(row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## 此函数从 CSV 文件中读取数据并将其存储为使用 __slots__ 的类的实例。
## 使用 __slots__ 的类经过内存优化,并且仍然提供命名访问。

## 测量内存使用的函数
def measure_memory(func, filename):
    tracemalloc.start()

    records = func(filename)

    current, peak = tracemalloc.get_traced_memory()

    ## 演示如何使用每种数据结构
    first_record = records[0]
    if func.__name__ == 'read_as_tuples':
        route, date, daytype, rides = first_record
    elif func.__name__ == 'read_as_dicts':
        route = first_record['route']
        date = first_record['date']
        daytype = first_record['daytype']
        rides = first_record['rides']
    else:  ## 命名元组和类
        route = first_record.route
        date = first_record.date
        daytype = first_record.daytype
        rides = first_record.rides

    print(f"Structure type: {func.__name__}")
    print(f"Record count: {len(records)}")
    print(f"Example access: Route={route}, Date={date}, Rides={rides}")
    print(f"Current memory: {current/1024/1024:.2f} MB")
    print(f"Peak memory: {peak/1024/1024:.2f} MB")
    print("-" * 50)

    tracemalloc.stop()

    return current

if __name__ == "__main__":
    filename = '/home/labex/project/ctabus.csv'

    ## 运行所有内存测试
    print("Memory usage comparison for different data structures:\n")

    results = []
    for reader_func in [
        read_as_tuples,
        read_as_dicts,
        read_as_named_tuples,
        read_as_regular_classes,
        read_as_slotted_classes
    ]:
        memory = measure_memory(reader_func, filename)
        results.append((reader_func.__name__, memory))

    ## 按内存使用排序(从低到高)
    results.sort(key=lambda x: x[1])

    print("\nRanking by memory efficiency (most efficient first):")
    for i, (name, memory) in enumerate(results, 1):
        print(f"{i}. {name}: {memory/1024/1024:.2f} MB")

运行脚本以查看比较结果:

python3 /home/labex/project/compare_structures.py

输出将显示每种数据结构的内存使用情况,以及从内存效率最高到最低的排名。

理解不同的数据结构

  1. 元组(Tuples)

    • 元组是轻量级且不可变的序列。这意味着一旦创建了元组,就不能更改其元素。
    • 你可以通过数字索引访问元组中的元素,例如 record[0]record[1] 等。
    • 它们的内存效率非常高,因为结构简单。
    • 然而,它们的可读性较差,因为你需要记住每个元素的索引。
  2. 字典(Dictionaries)

    • 字典使用键值对,这允许你通过名称访问元素。
    • 它们的可读性更强,例如,你可以使用 record['route']record['date'] 等。
    • 由于用于存储键值对的哈希表开销,它们的内存使用量较高。
    • 它们很灵活,因为你可以轻松地添加或删除字段。
  3. 命名元组(Named Tuples)

    • 命名元组结合了元组的效率和按名称访问元素的能力。
    • 你可以使用点号表示法访问元素,例如 record.routerecord.date 等。
    • 它们和普通元组一样是不可变的。
    • 它们的内存效率比字典高。
  4. 普通类(Regular Classes)

    • 普通类遵循面向对象的方法,具有命名属性。
    • 你可以使用点号表示法访问属性,例如 record.routerecord.date 等。
    • 你可以为普通类添加方法来定义行为。
    • 它们使用更多的内存,因为每个实例都有一个实例字典来存储其属性。
  5. 使用 __slots__ 的类(Classes with __slots__

    • 使用 __slots__ 的类是经过内存优化的类。它们避免使用实例字典,从而节省内存。
    • 它们仍然提供对属性的命名访问,例如 record.routerecord.date 等。
    • 它们限制在对象创建后添加新属性。
    • 它们的内存效率比普通类高。

何时使用每种方法

  • 元组(Tuples):当内存是关键因素,并且你只需要对数据进行简单的索引访问时,使用元组。
  • 字典(Dictionaries):当你需要灵活性,例如数据中的字段可能会变化时,使用字典。
  • 命名元组(Named Tuples):当你需要可读性和内存效率时,使用命名元组。
  • 普通类(Regular Classes):当你需要为数据添加行为(方法)时,使用普通类。
  • 使用 __slots__ 的类(Classes with __slots__:当你需要行为和最大内存效率时,使用使用 __slots__ 的类。

通过为你的需求选择合适的数据结构,你可以显著提高 Python 程序的性能和内存使用效率,尤其是在处理大型数据集时。

✨ 查看解决方案并练习

总结

在这个实验中,你学习了在 Python 中表示记录的不同方法,并分析了它们的内存效率。首先,你了解了基本的 CSV 数据集结构,并比较了原始文本存储方法。然后,你使用元组处理结构化数据,并实现了五种不同的数据结构:元组、字典、命名元组、普通类和使用 __slots__ 的类。

关键要点包括:不同的数据结构在内存效率、可读性和功能性之间存在权衡。Python 的对象开销对大型数据集的内存使用有显著影响,数据结构的选择会极大地影响内存消耗。命名元组和使用 __slots__ 的类是内存效率和代码可读性之间的良好折衷方案。这些概念对于从事数据处理的 Python 开发者来说非常有价值,尤其是在处理对内存效率要求极高的大型数据集时。