简介
在这个实验中,你将学习在 Python 中探索存储大型数据集的内存高效方法。你还将发现表示记录的不同方式,例如元组(tuples)、字典(dictionaries)、类(classes)和命名元组(named tuples)。
此外,你将比较不同数据结构的内存使用情况。理解这些结构之间的权衡对于进行数据分析的 Python 用户来说非常有价值,因为这有助于优化代码。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
在这个实验中,你将学习在 Python 中探索存储大型数据集的内存高效方法。你还将发现表示记录的不同方式,例如元组(tuples)、字典(dictionaries)、类(classes)和命名元组(named tuples)。
此外,你将比较不同数据结构的内存使用情况。理解这些结构之间的权衡对于进行数据分析的 Python 用户来说非常有价值,因为这有助于优化代码。
让我们从仔细研究将要使用的数据集开始我们的旅程。文件 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
函数打开文件,并将其赋值给变量 f
。 next
函数用于从文件中读取下一行。我们使用它三次:第一次读取标题行,标题行通常包含数据集中列的名称。第二次和第三次,我们分别读取第一条和第二条数据行。最后,我们使用 close
方法关闭文件,以释放系统资源。
你应该看到类似这样的输出:
route,date,daytype,rides
3,01/01/2001,U,7354
4,01/01/2001,U,9288
此输出显示该文件有 4 列数据。让我们分解一下每一列代表什么:
route
:这是公交线路名称或号码。它是数据集中的第一列(第 0 列)。date
:这是一个日期字符串,格式为 MM/DD/YYYY。这是第二列(第 1 列)。daytype
:这是一个日期类型代码,它是第三列(第 2 列)。
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 进行的内存分配。通过使用它,我们可以了解数据存储方法消耗了多少内存。
让我们先创建一个新的 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。
接下来,我们将测试另一种存储数据的方法。在同一个 /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 对象。每个对象都有一些额外的内存需求,包括:
另一方面,当你将整个文件内容存储为单个字符串时,只有一个对象,因此只有一组开销。考虑到数据的总大小,这种方式在内存使用上更高效。
在设计处理大型数据集的程序时,你需要考虑内存效率和数据可访问性之间的权衡。有时,将数据存储为字符串列表可能更便于访问,但会使用更多的内存。而在其他时候,你可能会优先考虑内存效率,选择将数据存储为单个字符串。
到目前为止,我们一直在处理原始文本数据的存储。但在进行数据分析时,我们通常需要将数据转换为更有条理和结构化的格式。这样可以更轻松地执行各种操作,并从数据中获取有价值的信息。在这一步中,我们将学习如何使用 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
的函数。下面是它的具体步骤:
filename
参数指定的 CSV 文件,这样我们就可以访问文件中的数据。csv
模块解析文件的每一行。csv.reader
函数帮助我们将每行拆分为单个值。rides
字段转换为整数。这是必要的,因为 CSV 文件中的数据最初是字符串格式,而我们需要一个数值进行计算。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
注意,与我们之前的示例相比,内存使用量增加了。这有几个原因:
使用这种方法的优点是,我们的数据现在已经正确结构化,并且可以进行分析。我们可以通过索引轻松访问每条记录的特定字段。例如:
## 访问元组元素的示例(将此代码添加到 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
输出将显示每种数据结构的内存使用情况,以及从内存效率最高到最低的排名。
元组(Tuples):
record[0]
、record[1]
等。字典(Dictionaries):
record['route']
、record['date']
等。命名元组(Named Tuples):
record.route
、record.date
等。普通类(Regular Classes):
record.route
、record.date
等。使用 __slots__
的类(Classes with __slots__
):
__slots__
的类是经过内存优化的类。它们避免使用实例字典,从而节省内存。record.route
、record.date
等。__slots__
的类(Classes with __slots__
):当你需要行为和最大内存效率时,使用使用 __slots__
的类。通过为你的需求选择合适的数据结构,你可以显著提高 Python 程序的性能和内存使用效率,尤其是在处理大型数据集时。
在这个实验中,你学习了在 Python 中表示记录的不同方法,并分析了它们的内存效率。首先,你了解了基本的 CSV 数据集结构,并比较了原始文本存储方法。然后,你使用元组处理结构化数据,并实现了五种不同的数据结构:元组、字典、命名元组、普通类和使用 __slots__
的类。
关键要点包括:不同的数据结构在内存效率、可读性和功能性之间存在权衡。Python 的对象开销对大型数据集的内存使用有显著影响,数据结构的选择会极大地影响内存消耗。命名元组和使用 __slots__
的类是内存效率和代码可读性之间的良好折衷方案。这些概念对于从事数据处理的 Python 开发者来说非常有价值,尤其是在处理对内存效率要求极高的大型数据集时。