Python 高阶函数

Beginner

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

简介

在本次实验中,你将学习 Python 中的高阶函数。高阶函数可以接受其他函数作为参数,或者将函数作为结果返回。这个概念在函数式编程中至关重要,它能让你编写更具模块化和可复用性的代码。

你将了解什么是高阶函数,创建一个接受函数作为参数的高阶函数,重构现有函数以使用高阶函数,并使用 Python 内置的 map() 函数。在实验过程中,你将对 reader.py 文件进行修改。

理解代码重复问题

让我们先看看 reader.py 文件中的现有代码。在编程中,检查现有代码是理解程序运行方式并找出改进点的重要步骤。你可以在 WebIDE 中打开 reader.py 文件,有两种方式可以实现。你可以在文件资源管理器中点击该文件,也可以在终端中运行以下命令。这些命令会先导航到项目目录,然后显示 reader.py 文件的内容。

cd ~/project
cat reader.py

当你查看代码时,会注意到有两个函数。Python 中的函数是执行特定任务的代码块。以下是这两个函数及其功能:

  1. csv_as_dicts():该函数接收 CSV 数据并将其转换为字典列表。Python 中的字典是键值对的集合,对于以结构化方式存储数据很有用。
  2. csv_as_instances():该函数接收 CSV 数据并将其转换为实例列表。实例是从类创建的对象,类是创建对象的蓝图。

现在,让我们更仔细地看看这两个函数。你会发现它们非常相似。这两个函数都遵循以下步骤:

  • 首先,它们初始化一个空的 records 列表。Python 中的列表是可以包含不同类型项的集合。初始化一个空列表意味着创建一个没有任何项的列表,用于存储处理后的数据。
  • 然后,它们使用 csv.reader() 来解析输入。解析意味着分析输入数据以提取有意义的信息。在这种情况下,csv.reader() 帮助我们逐行读取 CSV 数据。
  • 它们以相同的方式处理表头。CSV 文件中的表头是第一行,通常包含列名。
  • 之后,它们遍历 CSV 数据中的每一行。循环是一种编程结构,允许你多次执行一个代码块。
  • 对于每一行,它们对其进行处理以创建一条记录。这条记录可以是字典或实例,具体取决于函数。
  • 它们将记录追加到 records 列表中。追加意味着将一个项添加到列表的末尾。
  • 最后,它们返回 records 列表,其中包含所有处理后的数据。

代码重复会带来一些问题,原因如下:

  • 代码维护变得更加困难。如果你需要对代码进行更改,就必须在多个地方进行相同的更改,这会花费更多的时间和精力。
  • 任何更改都必须在多个地方实现,这增加了你可能会忘记在某个地方进行更改的可能性,从而导致行为不一致。
  • 这也增加了引入 bug 的可能性。Bug 是代码中的错误,可能会导致代码出现意外行为。

这两个函数之间唯一真正的区别在于它们如何将一行数据转换为一条记录。这是高阶函数非常有用的典型场景。高阶函数是可以接受另一个函数作为参数或返回一个函数作为结果的函数。

让我们看一些这些函数的示例用法,以便更好地理解它们的工作原理。以下代码展示了如何使用 csv_as_dicts()csv_as_instances()

## Example of using csv_as_dicts
with open('portfolio.csv') as f:
    portfolio = csv_as_dicts(f, [str, int, float])
print(portfolio[0])  ## {'name': 'AA', 'shares': 100, 'price': 32.2}

## Example of using csv_as_instances
class Stock:
    @classmethod
    def from_row(cls, row):
        return cls(row[0], int(row[1]), float(row[2]))

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

with open('portfolio.csv') as f:
    portfolio = csv_as_instances(f, Stock)
print(portfolio[0].name, portfolio[0].shares, portfolio[0].price)  ## AA 100 32.2

在下一步中,我们将创建一个高阶函数来消除这种代码重复。这将使代码更易于维护,并且更不容易出错。

创建高阶函数

在 Python 中,高阶函数是可以接受另一个函数作为参数的函数。这能带来更高的灵活性和代码复用性。现在,让我们创建一个名为 convert_csv() 的高阶函数。这个函数将处理处理 CSV 数据的常见操作,同时允许你自定义如何将 CSV 的每一行转换为一条记录。

在 WebIDE 中打开 reader.py 文件。我们将添加一个函数,该函数将接受一个 CSV 数据的可迭代对象、一个转换函数,还可以选择传入列名。转换函数将用于把 CSV 的每一行转换为一条记录。

以下是 convert_csv() 函数的代码。将其复制并粘贴到你的 reader.py 文件中:

def convert_csv(lines, conversion_func, *, headers=None):
    '''
    Convert lines of CSV data using the provided conversion function

    Args:
        lines: An iterable containing CSV data
        conversion_func: A function that takes headers and a row and returns a record
        headers: Column headers (optional). If None, the first row is used as headers

    Returns:
        A list of records as processed by conversion_func
    '''
    records = []
    rows = csv.reader(lines)
    if headers is None:
        headers = next(rows)
    for row in rows:
        record = conversion_func(headers, row)
        records.append(record)
    return records

让我们来详细分析这个函数的功能。首先,它初始化一个名为 records 的空列表,用于存储转换后的记录。然后,它使用 csv.reader() 函数读取 CSV 数据行。如果没有提供列名,它会将第一行作为列名。对于后续的每一行,它会应用 conversion_func 函数将该行转换为一条记录,并将其添加到 records 列表中。最后,它返回记录列表。

现在,我们需要一个简单的转换函数来测试我们的 convert_csv() 函数。这个函数将接受列名和一行数据,并使用列名作为键将该行转换为一个字典。

以下是 make_dict() 函数的代码。也将这个函数添加到你的 reader.py 文件中:

def make_dict(headers, row):
    '''
    Convert a row to a dictionary using the provided headers
    '''
    return dict(zip(headers, row))

make_dict() 函数使用 zip() 函数将每个列名与该行中对应的数值配对,然后从这些配对中创建一个字典。

让我们来测试这些函数。在终端中运行以下命令来打开一个 Python 交互式 shell:

cd ~/project
python3 -i reader.py

python3 命令中的 -i 选项以交互模式启动 Python 解释器,并导入 reader.py 文件,这样我们就可以使用刚刚定义的函数。

在 Python shell 中,运行以下代码来测试我们的函数:

## Open the CSV file
lines = open('portfolio.csv')

## Convert to a list of dictionaries using our new function
result = convert_csv(lines, make_dict)

## Print the result
print(result)

这段代码打开 portfolio.csv 文件,使用 convert_csv() 函数和 make_dict() 转换函数将 CSV 数据转换为字典列表,然后打印结果。

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

[{'name': 'AA', 'shares': '100', 'price': '32.20'}, {'name': 'IBM', 'shares': '50', 'price': '91.10'}, {'name': 'CAT', 'shares': '150', 'price': '83.44'}, {'name': 'MSFT', 'shares': '200', 'price': '51.23'}, {'name': 'GE', 'shares': '95', 'price': '40.37'}, {'name': 'MSFT', 'shares': '50', 'price': '65.10'}, {'name': 'IBM', 'shares': '100', 'price': '70.44'}]

这个输出表明我们的高阶函数 convert_csv() 正常工作。我们成功创建了一个接受另一个函数作为参数的函数,这使我们能够轻松改变 CSV 数据的转换方式。

要退出 Python shell,你可以输入 exit() 或按 Ctrl+D。

重构现有函数

现在,我们已经创建了一个名为 convert_csv() 的高阶函数。高阶函数是可以接受其他函数作为参数或返回函数作为结果的函数。它们是 Python 中一个强大的概念,能帮助我们编写更具模块化和可复用性的代码。在本节中,我们将使用这个高阶函数来重构原始的 csv_as_dicts()csv_as_instances() 函数。重构是在不改变现有代码外部行为的前提下对其进行结构调整的过程,目的是改善其内部结构,例如消除代码重复。

让我们先在 WebIDE 中打开 reader.py 文件。我们将按以下方式更新这些函数:

  1. 首先,我们将替换 csv_as_dicts() 函数。这个函数用于将 CSV 数据行转换为字典列表。以下是新代码:
def csv_as_dicts(lines, types, *, headers=None):
    '''
    Convert lines of CSV data into a list of dictionaries
    '''
    def dict_converter(headers, row):
        return {name: func(val) for name, func, val in zip(headers, types, row)}

    return convert_csv(lines, dict_converter, headers=headers)

在这段代码中,我们定义了一个内部函数 dict_converter,它接受 headersrow 作为参数。它使用字典推导式创建一个字典,其中键是列名,值是将相应的类型转换函数应用于行中值的结果。然后,我们将 dict_converter 函数作为参数调用 convert_csv() 函数。

  1. 接下来,我们将替换 csv_as_instances() 函数。这个函数用于将 CSV 数据行转换为给定类的实例列表。以下是新代码:
def csv_as_instances(lines, cls, *, headers=None):
    '''
    Convert lines of CSV data into a list of instances
    '''
    def instance_converter(headers, row):
        return cls.from_row(row)

    return convert_csv(lines, instance_converter, headers=headers)

在这段代码中,我们定义了一个内部函数 instance_converter,它接受 headersrow 作为参数。它调用给定类 clsfrom_row 类方法,从行数据创建一个实例。然后,我们将 instance_converter 函数作为参数调用 convert_csv() 函数。

重构这些函数后,我们需要对它们进行测试,以确保它们仍能按预期工作。为此,我们将在 Python shell 中运行以下命令:

cd ~/project
python3 -i reader.py

cd ~/project 命令将当前工作目录更改为 project 目录。python3 -i reader.py 命令以交互模式运行 reader.py 文件,这意味着在文件运行结束后,我们可以继续执行 Python 代码。

Python shell 打开后,我们将运行以下代码来测试重构后的函数:

## Define a simple Stock class for testing
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @classmethod
    def from_row(cls, row):
        return cls(row[0], int(row[1]), float(row[2]))

    def __repr__(self):
        return f'Stock({self.name}, {self.shares}, {self.price})'

## Test csv_as_dicts
with open('portfolio.csv') as f:
    portfolio_dicts = csv_as_dicts(f, [str, int, float])
print("First dictionary:", portfolio_dicts[0])

## Test csv_as_instances
with open('portfolio.csv') as f:
    portfolio_instances = csv_as_instances(f, Stock)
print("First instance:", portfolio_instances[0])

在这段代码中,我们首先定义了一个简单的 Stock 类用于测试。__init__ 方法初始化 Stock 实例的属性。from_row 类方法从 CSV 数据行创建一个 Stock 实例。__repr__ 方法提供 Stock 实例的字符串表示形式。

然后,我们通过打开 portfolio.csv 文件并将其与类型转换函数列表一起传递给 csv_as_dicts() 函数来测试该函数。我们打印结果列表中的第一个字典。

最后,我们通过打开 portfolio.csv 文件并将其与 Stock 类一起传递给 csv_as_instances() 函数来测试该函数。我们打印结果列表中的第一个实例。

如果一切正常,你应该会看到类似于以下的输出:

First dictionary: {'name': 'AA', 'shares': 100, 'price': 32.2}
First instance: Stock(AA, 100, 32.2)

这个输出表明我们重构后的函数工作正常。我们成功消除了代码重复,同时保持了相同的功能。

要退出 Python shell,你可以输入 exit() 或按 Ctrl+D。

使用 map() 函数

在 Python 中,高阶函数是可以接受另一个函数作为参数或返回一个函数作为结果的函数。Python 的 map() 函数就是高阶函数的一个很好的例子。它是一个强大的工具,允许你将给定的函数应用于可迭代对象(如列表或元组)中的每个元素。将函数应用于每个元素后,它会返回一个包含结果的迭代器。这个特性使 map() 非常适合处理数据序列,比如 CSV 文件中的行。

map() 函数的基本语法如下:

map(function, iterable, ...)

这里,function 是你想要对 iterable 中的每个元素执行的操作。iterable 是一个元素序列,如列表或元组。

让我们看一个简单的例子。假设你有一个数字列表,你想对列表中的每个数字进行平方运算。你可以使用 map() 函数来实现这一点。以下是具体做法:

numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x * x, numbers))
print(squared)  ## Output: [1, 4, 9, 16, 25]

在这个例子中,我们首先定义了一个名为 numbers 的列表。然后,我们使用 map() 函数。lambda 函数 lambda x: x * x 是我们想要对 numbers 列表中的每个元素执行的操作。map() 函数将这个 lambda 函数应用于列表中的每个数字。由于 map() 返回一个迭代器,我们使用 list() 函数将其转换为列表。最后,我们打印 squared 列表,它包含了原始数字的平方值。

现在,让我们看看如何使用 map() 函数来修改我们的 convert_csv() 函数。之前,我们使用 for 循环来遍历 CSV 数据中的行。现在,我们将用 map() 函数替换那个 for 循环。

def convert_csv(lines, conversion_func, *, headers=None):
    '''
    Convert lines of CSV data using the provided conversion function
    '''
    rows = csv.reader(lines)
    if headers is None:
        headers = next(rows)

    ## Use map to apply conversion_func to each row
    records = list(map(lambda row: conversion_func(headers, row), rows))
    return records

这个更新后的 convert_csv() 函数与之前的版本功能完全相同,但它使用 map() 函数代替了 for 循环。map() 内部的 lambda 函数从 CSV 数据中取出每一行,并将 conversion_func 应用于该行以及列名。

让我们测试这个更新后的函数,确保它能正常工作。首先,打开终端并导航到项目目录。然后,使用 reader.py 文件启动 Python 交互式 shell。

cd ~/project
python3 -i reader.py

进入 Python shell 后,运行以下代码来测试更新后的 convert_csv() 函数:

## Test the updated convert_csv function
with open('portfolio.csv') as f:
    result = convert_csv(f, make_dict)
print(result[0])  ## Should print the first dictionary

## Test that csv_as_dicts still works
with open('portfolio.csv') as f:
    portfolio = csv_as_dicts(f, [str, int, float])
print(portfolio[0])  ## Should print the first dictionary with converted types

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

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

这个输出表明,使用 map() 函数更新后的 convert_csv() 函数能正常工作,并且依赖于它的函数也能继续按预期工作。

使用 map() 函数有几个优点:

  1. 它比 for 循环更简洁。你无需为 for 循环编写多行代码,使用 map() 只需一行代码就能达到相同的效果。
  2. 它能清晰地传达你要对序列中的每个元素进行转换的意图。当你看到 map() 时,你立刻就知道你正在将一个函数应用于可迭代对象中的每个元素。
  3. 它可能更节省内存,因为它返回一个迭代器。迭代器会即时生成值,这意味着它不会一次性将所有结果存储在内存中。在我们的例子中,我们将 map() 返回的迭代器转换为了列表,但在某些情况下,你可以直接使用迭代器来节省内存。

要退出 Python shell,你可以输入 exit() 或按 Ctrl+D。

总结

在本次实验中,你学习了 Python 中的高阶函数,以及它们如何有助于编写更具模块化和可维护性的代码。首先,你识别出了两个相似函数中的代码重复问题。接着,你创建了一个高阶函数 convert_csv(),它接受一个转换函数作为参数,并重构了原始函数以使用该高阶函数。最后,你更新了这个高阶函数,使其利用 Python 的内置 map() 函数。

这些技术是 Python 程序员工具包中的强大资产。高阶函数促进了代码复用和关注点分离,而将函数作为参数传递则允许实现更灵活和可定制的行为。像 map() 这样的函数提供了简洁的数据转换方式。掌握这些概念能让你编写更简洁、更易维护且更不易出错的 Python 代码。