探索 Python 的一等对象与内存模型

PythonPythonBeginner
立即练习

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

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

简介

在这个实验中,你将了解 Python 的一等对象(first - class object)概念,并探索其内存模型。Python 将函数、类型和数据视为一等对象,这使得编程模式强大且灵活。

你还将创建可复用的实用函数来处理 CSV 数据。具体来说,你将在 reader.py 文件中创建一个通用函数,用于读取 CSV 数据,该函数可在不同项目中复用。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/BasicConceptsGroup(["Basic Concepts"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ModulesandPackagesGroup(["Modules and Packages"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/FileHandlingGroup(["File Handling"]) python(("Python")) -.-> python/PythonStandardLibraryGroup(["Python Standard Library"]) python/BasicConceptsGroup -.-> python/strings("Strings") python/BasicConceptsGroup -.-> python/type_conversion("Type Conversion") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/scope("Scope") python/ModulesandPackagesGroup -.-> python/importing_modules("Importing Modules") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/FileHandlingGroup -.-> python/file_operations("File Operations") python/PythonStandardLibraryGroup -.-> python/data_collections("Data Collections") subgraph Lab Skills python/strings -.-> lab-132489{{"探索 Python 的一等对象与内存模型"}} python/type_conversion -.-> lab-132489{{"探索 Python 的一等对象与内存模型"}} python/function_definition -.-> lab-132489{{"探索 Python 的一等对象与内存模型"}} python/scope -.-> lab-132489{{"探索 Python 的一等对象与内存模型"}} python/importing_modules -.-> lab-132489{{"探索 Python 的一等对象与内存模型"}} python/classes_objects -.-> lab-132489{{"探索 Python 的一等对象与内存模型"}} python/file_operations -.-> lab-132489{{"探索 Python 的一等对象与内存模型"}} python/data_collections -.-> lab-132489{{"探索 Python 的一等对象与内存模型"}} end

理解 Python 中的一等对象

在 Python 中,一切都被视为对象,这包括函数和类型。这意味着什么呢?这意味着你可以将函数和类型存储在数据结构中,将它们作为参数传递给其他函数,甚至从其他函数中返回它们。这是一个非常强大的概念,我们将以 CSV 数据处理为例来探索它。

探索一等类型

首先,让我们启动 Python 解释器。在 WebIDE 中打开一个新的终端,然后输入以下命令。这个命令将启动 Python 解释器,我们将在其中运行 Python 代码。

python3

在 Python 中处理 CSV 文件时,我们经常需要将从这些文件中读取的字符串转换为合适的数据类型。例如,CSV 文件中的数字可能会被读取为字符串,但我们希望在 Python 代码中把它作为整数或浮点数使用。为此,我们可以创建一个转换函数列表。

coltypes = [str, int, float]

注意,我们创建的列表中包含的是实际的类型函数,而不是字符串。在 Python 中,类型是一等对象,这意味着我们可以像对待其他任何对象一样对待它们。我们可以将它们放入列表中、传递它们,并在代码中使用它们。

现在,让我们从一个投资组合(portfolio)的 CSV 文件中读取一些数据,看看如何使用这些转换函数。

import csv
f = open('portfolio.csv')
rows = csv.reader(f)
headers = next(rows)
row = next(rows)
print(row)

当你运行这段代码时,你应该会看到类似于以下的输出。这是 CSV 文件中的第一行数据,以字符串列表的形式表示。

['AA', '100', '32.20']

接下来,我们将使用 zip 函数。zip 函数接受多个可迭代对象(如列表或元组),并将它们的元素配对。我们将使用它把行中的每个值与其对应的类型转换函数配对。

r = list(zip(coltypes, row))
print(r)

这将产生以下输出。每一对都包含一个类型函数和 CSV 文件中的一个字符串值。

[(<class 'str'>, 'AA'), (<class 'int'>, '100'), (<class 'float'>, '32.20')]

现在我们有了这些配对,就可以应用每个函数将值转换为合适的类型。

record = [func(val) for func, val in zip(coltypes, row)]
print(record)

输出将显示这些值已被转换为合适的类型。字符串 'AA' 仍然是字符串,'100' 变成了整数 100,'32.20' 变成了浮点数 32.2。

['AA', 100, 32.2]

我们还可以将这些值与它们的列名组合起来,创建一个字典。字典是 Python 中一种有用的数据结构,它允许我们存储键值对。

record_dict = dict(zip(headers, record))
print(record_dict)

输出将是一个字典,其中键是列名,值是转换后的数据。

{'name': 'AA', 'shares': 100, 'price': 32.2}

你可以使用单个推导式(comprehension)完成所有这些步骤。推导式是在 Python 中创建列表、字典或集合的简洁方式。

result = {name: func(val) for name, func, val in zip(headers, coltypes, row)}
print(result)

输出将与之前的字典相同。

{'name': 'AA', 'shares': 100, 'price': 32.2}

当你在 Python 解释器中完成工作后,可以通过输入以下命令退出。

exit()

这个演示展示了 Python 将函数视为一等对象如何实现强大的数据处理技术。通过能够将类型和函数视为对象,我们可以编写更灵活、更简洁的代码。

创建用于 CSV 处理的实用函数

既然我们已经了解了 Python 的一等对象如何帮助我们进行数据转换,接下来我们将创建一个可复用的实用函数。这个函数将读取 CSV 数据并将其转换为字典列表。这是一个非常有用的操作,因为 CSV 文件通常用于存储表格数据,将其转换为字典列表可以让我们在 Python 中更轻松地处理这些数据。

创建 CSV 读取器实用函数

首先,打开 WebIDE。打开后,导航到项目目录并创建一个名为 reader.py 的新文件。在这个文件中,我们将定义一个读取 CSV 数据并进行类型转换的函数。类型转换很重要,因为 CSV 文件中的数据通常以字符串形式读取,但我们可能需要不同的数据类型(如整数或浮点数)进行进一步处理。

将以下代码添加到 reader.py 中:

import csv

def read_csv_as_dicts(filename, types):
    """
    Read a CSV file into a list of dictionaries, converting each field according
    to the types provided.

    Parameters:
    filename (str): Name of the CSV file to read
    types (list): List of type conversion functions for each column

    Returns:
    list: List of dictionaries representing the CSV data
    """
    records = []
    with open(filename, 'r') as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Get the column headers

        for row in rows:
            ## Apply type conversions to each value in the row
            converted_row = [func(val) for func, val in zip(types, row)]

            ## Create a dictionary mapping headers to converted values
            record = dict(zip(headers, converted_row))
            records.append(record)

    return records

这个函数首先打开指定的 CSV 文件,然后读取 CSV 文件的表头(即列名)。接着,它会遍历文件中的每一行。对于行中的每个值,它会应用 types 列表中对应的类型转换函数。最后,它会创建一个字典,其中键是列名,值是转换后的数据,并将这个字典添加到 records 列表中。处理完所有行后,它会返回 records 列表。

测试实用函数

让我们来测试一下我们的实用函数。首先,打开一个终端并输入以下命令启动 Python 解释器:

python3

现在我们已经进入了 Python 解释器,可以使用我们的函数来读取投资组合(portfolio)数据。投资组合数据是一个 CSV 文件,包含股票信息,如股票名称、股数和价格。

import reader
portfolio = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
for record in portfolio[:3]:  ## Show the first 3 records
    print(record)

当你运行这段代码时,你应该会看到类似于以下的输出:

{'name': 'AA', 'shares': 100, 'price': 32.2}
{'name': 'IBM', 'shares': 50, 'price': 91.1}
{'name': 'CAT', 'shares': 150, 'price': 83.44}

这个输出显示了投资组合数据中的前三条记录,并且数据类型已经正确转换。

让我们再用芝加哥交通管理局(CTA)的公交数据来测试一下我们的函数。CTA 公交数据是另一个 CSV 文件,包含公交线路、日期、工作日类型和乘车人数等信息。

rows = reader.read_csv_as_dicts('ctabus.csv', [str, str, str, int])
print(f"Total rows: {len(rows)}")
print("First row:", rows[0])

输出应该类似于:

Total rows: 577563
First row: {'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}

这表明我们的函数可以处理不同的 CSV 文件并进行适当的类型转换。

要退出 Python 解释器,请输入:

exit()

现在你已经创建了一个可复用的实用函数,它可以读取任何 CSV 文件并进行适当的类型转换。这展示了 Python 一等对象的强大之处,以及如何利用它们来创建灵活、可复用的代码。

✨ 查看解决方案并练习

探索 Python 的内存模型

Python 的内存模型在决定对象如何存储在内存中以及如何被引用方面起着至关重要的作用。理解这个模型非常重要,尤其是在处理大型数据集时,因为它会显著影响 Python 程序的性能和内存使用情况。在这一步中,我们将特别关注 Python 如何处理字符串对象,并探索优化大型数据集内存使用的方法。

数据集中的字符串重复问题

芝加哥交通管理局(CTA)的公交数据包含许多重复的值,例如公交线路名称。如果处理不当,数据集中的重复值可能会导致内存使用效率低下。为了了解这个问题的严重程度,让我们首先检查数据集中有多少个唯一的公交线路字符串。

首先,打开 Python 解释器。你可以在终端中运行以下命令来实现:

python3

Python 解释器打开后,我们将加载 CTA 公交数据并找出唯一的公交线路。以下是实现此功能的代码:

import reader
rows = reader.read_csv_as_dicts('ctabus.csv', [str, str, str, int])

## Find unique route names
routes = {row['route'] for row in rows}
print(f"Number of unique route names: {len(routes)}")

在这段代码中,我们首先导入 reader 模块,该模块可能包含一个将 CSV 文件读取为字典的函数。然后,我们使用 read_csv_as_dicts 函数从 ctabus.csv 文件中加载数据。第二个参数 [str, str, str, int] 指定了 CSV 文件中每列的数据类型。之后,我们使用集合推导式找出数据集中所有唯一的公交线路名称,并打印出唯一公交线路名称的数量。

输出应该是:

Number of unique route names: 181

现在,让我们检查为这些公交线路创建了多少个不同的字符串对象。尽管只有 181 个唯一的公交线路名称,但 Python 可能会为数据集中每次出现的公交线路名称创建一个新的字符串对象。为了验证这一点,我们将使用 id() 函数获取每个字符串对象的唯一标识符。

## Count unique string object IDs
routeids = {id(row['route']) for row in rows}
print(f"Number of unique route string objects: {len(routeids)}")

输出可能会让你感到惊讶:

Number of unique route string objects: 542305

这表明只有 181 个唯一的公交线路名称,但有超过 500,000 个唯一的字符串对象。这是因为即使值相同,Python 也会为每一行创建一个新的字符串对象。这可能会导致大量的内存浪费,尤其是在处理大型数据集时。

使用字符串驻留(String Interning)节省内存

Python 提供了一种使用 sys.intern() 函数来“驻留”(复用)字符串的方法。当数据集中有许多重复的字符串时,字符串驻留可以节省内存。当你对一个字符串进行驻留操作时,Python 会检查 intern 池(字符串驻留池)中是否已经存在相同的字符串。如果存在,它将返回对现有字符串对象的引用,而不是创建一个新的对象。

让我们通过一个简单的例子来演示字符串驻留的工作原理:

import sys

## Without interning
a = 'hello world'
b = 'hello world'
print(f"a is b (without interning): {a is b}")

## With interning
a = sys.intern(a)
b = sys.intern(b)
print(f"a is b (with interning): {a is b}")

在这段代码中,我们首先创建了两个具有相同值的字符串变量 ab,但没有进行驻留操作。is 运算符用于检查两个变量是否引用同一个对象。在没有驻留的情况下,ab 是不同的对象,因此 a is b 返回 False。然后,我们使用 sys.intern() 对两个字符串进行驻留操作。驻留后,ab 引用 intern 池中的同一个对象,因此 a is b 返回 True

输出应该是:

a is b (without interning): False
a is b (with interning): True

现在,让我们在读取 CTA 公交数据时使用字符串驻留来减少内存使用。我们还将使用 tracemalloc 模块来跟踪驻留前后的内存使用情况。

import sys
import reader
import tracemalloc

## Start memory tracking
tracemalloc.start()

## Read data with interning for the route column
rows = reader.read_csv_as_dicts('ctabus.csv', [sys.intern, str, str, int])

## Check unique route objects again
routeids = {id(row['route']) for row in rows}
print(f"Number of unique route string objects (with interning): {len(routeids)}")

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")

在这段代码中,我们首先使用 tracemalloc.start() 开始内存跟踪。然后,我们通过将 sys.intern 作为第一列的数据类型,对公交线路列进行驻留操作来读取 CTA 公交数据。之后,我们再次检查唯一的公交线路字符串对象的数量,并打印当前和峰值内存使用情况。

输出应该类似于:

Number of unique route string objects (with interning): 181
Current memory usage: 189.56 MB
Peak memory usage: 209.32 MB

让我们重启解释器,尝试对公交线路和日期字符串都进行驻留操作,看看是否可以进一步减少内存使用。

exit()

再次启动 Python:

python3
import sys
import reader
import tracemalloc

## Start memory tracking
tracemalloc.start()

## Read data with interning for both route and date columns
rows = reader.read_csv_as_dicts('ctabus.csv', [sys.intern, sys.intern, str, int])

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage (interning route and date): {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage (interning route and date): {peak / 1024 / 1024:.2f} MB")

输出应该显示内存使用进一步减少:

Current memory usage (interning route and date): 170.23 MB
Peak memory usage (interning route and date): 190.05 MB

这展示了理解 Python 的内存模型并使用字符串驻留等技术如何帮助优化程序,尤其是在处理包含重复值的大型数据集时。

最后,退出 Python 解释器:

exit()

面向列的数据存储

到目前为止,我们一直将 CSV 数据存储为行字典列表。这意味着 CSV 文件中的每一行都表示为一个字典,其中键是列标题,值是该行中的相应数据。然而,在处理大型数据集时,这种方法可能效率不高。以面向列的格式存储数据可能是更好的选择。在面向列的方法中,每列的数据都存储在一个单独的列表中。这可以显著减少内存使用,因为相似的数据类型被分组在一起,并且对于某些按列聚合数据的操作,还可以提高性能。

创建面向列的数据读取器

现在,我们将创建一个新文件,帮助我们以面向列的格式读取 CSV 数据。在项目目录中创建一个名为 colreader.py 的新文件,并添加以下代码:

import csv

class DataCollection:
    def __init__(self, headers, columns):
        """
        Initialize a column-oriented data collection.

        Parameters:
        headers (list): Column header names
        columns (dict): Dictionary mapping header names to column data lists
        """
        self.headers = headers
        self.columns = columns
        self._length = len(columns[headers[0]]) if headers else 0

    def __len__(self):
        """Return the number of rows in the collection."""
        return self._length

    def __getitem__(self, index):
        """
        Get a row by index, presented as a dictionary.

        Parameters:
        index (int): Row index

        Returns:
        dict: Dictionary representing the row at the given index
        """
        if isinstance(index, int):
            if index < 0 or index >= self._length:
                raise IndexError("Index out of range")

            return {header: self.columns[header][index] for header in self.headers}
        else:
            raise TypeError("Index must be an integer")

def read_csv_as_columns(filename, types):
    """
    Read a CSV file into a column-oriented data structure, converting each field
    according to the types provided.

    Parameters:
    filename (str): Name of the CSV file to read
    types (list): List of type conversion functions for each column

    Returns:
    DataCollection: Column-oriented data collection representing the CSV data
    """
    with open(filename, 'r') as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Get the column headers

        ## Initialize columns
        columns = {header: [] for header in headers}

        ## Read data into columns
        for row in rows:
            ## Convert values according to the specified types
            converted_values = [func(val) for func, val in zip(types, row)]

            ## Add each value to its corresponding column
            for header, value in zip(headers, converted_values):
                columns[header].append(value)

    return DataCollection(headers, columns)

这段代码做了两件重要的事情:

  1. 定义了一个 DataCollection 类。这个类以列的形式存储数据,但允许我们像处理行字典列表一样访问数据。这很有用,因为它提供了一种熟悉的方式来处理数据。
  2. 定义了一个 read_csv_as_columns 函数。这个函数从文件中读取 CSV 数据,并将其存储在面向列的结构中。它还根据我们提供的类型转换 CSV 文件中的每个字段。

测试面向列的读取器

让我们使用 CTA 公交数据来测试我们的面向列的读取器。首先,打开 Python 解释器。你可以在终端中运行以下命令来实现:

python3

Python 解释器打开后,运行以下代码:

import colreader
import tracemalloc
from sys import intern

## Start memory tracking
tracemalloc.start()

## Read data into column-oriented structure with string interning
data = colreader.read_csv_as_columns('ctabus.csv', [intern, intern, intern, int])

## Check that we can access the data like a list of dictionaries
print(f"Number of rows: {len(data)}")
print("First 3 rows:")
for i in range(3):
    print(data[i])

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")

输出应该如下所示:

Number of rows: 577563
First 3 rows:
{'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}
{'route': '4', 'date': '01/01/2001', 'daytype': 'U', 'rides': 9288}
{'route': '6', 'date': '01/01/2001', 'daytype': 'U', 'rides': 6048}
Current memory usage: 38.67 MB
Peak memory usage: 103.42 MB

现在,让我们将其与之前的面向行的方法进行比较。在同一个 Python 解释器中运行以下代码:

import reader
import tracemalloc
from sys import intern

## Reset memory tracking
tracemalloc.reset_peak()

## Read data into row-oriented structure with string interning
rows = reader.read_csv_as_dicts('ctabus.csv', [intern, intern, intern, int])

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage (row-oriented): {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage (row-oriented): {peak / 1024 / 1024:.2f} MB")

输出应该类似于:

Current memory usage (row-oriented): 170.23 MB
Peak memory usage (row-oriented): 190.05 MB

如你所见,面向列的方法使用的内存显著减少!

让我们还测试一下我们仍然可以像以前一样分析数据。运行以下代码:

## Find all unique routes in the column-oriented data
routes = {row['route'] for row in data}
print(f"Number of unique routes: {len(routes)}")

## Count rides per route (first 5)
from collections import defaultdict
route_rides = defaultdict(int)
for row in data:
    route_rides[row['route']] += row['rides']

## Show the top 5 routes by total rides
top_routes = sorted(route_rides.items(), key=lambda x: x[1], reverse=True)[:5]
print("Top 5 routes by total rides:")
for route, rides in top_routes:
    print(f"Route {route}: {rides:,} rides")

输出应该是:

Number of unique routes: 181
Top 5 routes by total rides:
Route 9: 158,545,826 rides
Route 49: 129,872,910 rides
Route 77: 120,086,065 rides
Route 79: 109,348,708 rides
Route 4: 91,405,538 rides

最后,通过运行以下命令退出 Python 解释器:

exit()

我们可以看到,面向列的方法不仅节省了内存,还允许我们执行与以前相同的分析。这表明不同的数据存储策略如何在为我们提供相同的数据操作接口的同时,对性能产生重大影响。

✨ 查看解决方案并练习

总结

在这个实验中,你学习了几个关键的 Python 概念。首先,你了解了 Python 如何将函数、类型和其他实体视为一等对象(first-class objects),从而可以像处理普通数据一样传递和存储它们。其次,你创建了可复用的实用函数,用于自动进行类型转换的 CSV 数据处理。

此外,你探索了 Python 的内存模型,并使用字符串驻留(string interning)来减少重复数据的内存使用。你还为大型数据集实现了一种更高效的面向列的存储方法,同时提供了熟悉的用户界面。这些概念展示了 Python 在数据处理方面的灵活性和强大功能,并且这些技术可以应用于实际的数据分析师项目中。