第一章:函数式编程的基石与核心哲学
函数式编程(Functional Programming, FP)并非一种全新的编程语言,而是一种编程范式,一种思考和构造程序的方式。它根植于数学中的函数概念,强调程序的执行过程应如同数学函数的求值,避免状态的改变和可变数据。要真正掌握Python中的函数式编程,我们必须首先从其最底层的基石和核心哲学开始理解。
1.1 什么是编程范式?函数式编程的宏观定位
在深入理解函数式编程之前,我们有必要先明确“编程范式”(Programming Paradigm)这一概念。编程范式是指一种编写代码和组织程序思想的“风格”或“方法论”。它提供了一套原则和结构,指导开发者如何思考问题、如何设计解决方案以及如何实现代码。
常见的编程范式包括:
- 命令式编程(Imperative Programming):关注“如何做”(how to do)。通过改变程序状态的语句(如赋值、循环、条件判断)来描述计算机执行的具体步骤。大多数传统编程语言(如C、Fortran)都以内置命令式特性为主。
- 声明式编程(Declarative Programming):关注“做什么”(what to do),而不是“如何做”。开发者描述期望的结果,而将实现细节交给语言或框架。SQL查询、HTML标记语言、正则表达式等都属于声明式编程范畴。
- 面向对象编程(Object-Oriented Programming, OOP):将程序视为一系列相互作用的“对象”,每个对象封装了数据(属性)和行为(方法)。它强调封装、继承和多态。Python、Java、C++ 等是典型的面向对象语言。
- 函数式编程(Functional Programming, FP):将计算视为数学函数的求值,避免状态改变和可变数据。它强调使用纯函数、不可变性、一等函数和高阶函数。
函数式编程在Python中并非强制性范式,Python是一种多范式语言,这意味着它允许开发者根据具体问题和个人偏好,选择或结合不同的编程范式。在Python中学习函数式编程,不是要抛弃命令式或面向对象编程,而是要学习如何利用函数式编程的优点,编写更清晰、更可维护、更易于测试的代码。
1.2 函数式编程的核心哲学:纯函数(Pure Functions)
纯函数是函数式编程的灵魂和基石。理解并能够编写纯函数,是迈入函数式编程大门的第一步。
纯函数的定义:
一个函数如果满足以下两个条件,则称之为纯函数:
- 相同的输入,总能得到相同的输出(确定性):给定相同的参数,函数总是返回相同的结果,不受外部状态或时间的影响。
- 无副作用(No Side Effects):函数不会引起在函数外部可观察到的任何状态变化。这意味着函数不会修改全局变量、不会修改传入的参数对象、不会进行I/O操作(如打印到控制台、读写文件、网络请求)、不会修改数据库等。
让我们通过代码示例来深入理解这两个条件。
1.2.1 相同的输入,相同的输出(确定性)
# 示例 1-1:纯函数的确定性 - 数学计算
# 这是一个计算两个数之和的函数
def pure_add(a: int, b: int) -> int:
"""
一个计算两个整数之和的纯函数。
给定相同的输入a和b,它总是返回相同的结果。
"""
# 返回a和b的和
return a + b # 将两个输入参数相加并返回结果
# 调用纯函数进行计算
result1 = pure_add(5, 3) # 调用pure_add函数,输入5和3
print(f"纯函数 pure_add(5, 3) 的结果: {
result1}") # 打印pure_add函数的结果
result2 = pure_add(5, 3) # 再次调用pure_add函数,输入相同的5和3
print(f"再次调用 pure_add(5, 3) 的结果: {
result2}") # 打印第二次调用pure_add函数的结果
# 验证确定性:结果总是相同
assert result1 == result2 # 断言两次调用结果相同,证明其确定性
# 示例 1-2:非纯函数 - 引入不确定性
# 这是一个会依赖外部状态的非纯函数
global_counter = 0 # 定义一个全局计数器变量
# 这个函数会增加全局计数器,导致其输出不确定
def impure_increment_and_add(a: int, b: int) -> int:
"""
一个非纯函数,因为它依赖并修改了外部全局状态。
虽然它也计算a和b的和,但每次调用其行为可能不同。
"""
# 声明global_counter为全局变量,以便在函数内部修改它
global global_counter # 声明global_counter是全局变量
# 增加全局计数器
global_counter += 1 # 全局计数器加1
# 返回a和b的和加上全局计数器
return a + b + global_counter # 将a、b和全局计数器相加并返回
print(f"\n--- 非纯函数的确定性问题 ---") # 打印非纯函数确定性问题标题
print(f"初始 global_counter: {
global_counter}") # 打印global_counter的初始值
# 第一次调用非纯函数
result_impure1 = impure_increment_and_add(5, 3) # 调用impure_increment_and_add函数,输入5和3
print(f"第一次调用 impure_increment_and_add(5, 3) 的结果: {
result_impure1}") # 打印第一次调用结果
print(f"调用后 global_counter: {
global_counter}") # 打印调用后global_counter的值
# 第二次调用非纯函数
result_impure2 = impure_increment_and_add(5, 3) # 再次调用impure_increment_and_add函数,输入相同的5和3
print(f"第二次调用 impure_increment_and_add(5, 3) 的结果: {
result_impure2}") # 打印第二次调用结果
print(f"调用后 global_counter: {
global_counter}") # 打印第二次调用后global_counter的值
# 验证不确定性:结果可能不同
assert result_impure1 != result_impure2 # 断言两次调用结果不同,证明其不确定性
# 示例 1-3:非纯函数 - 依赖时间
import datetime # 导入datetime模块
# 这个函数会依赖当前时间,导致其输出不确定
def impure_log_time_and_return(message: str) -> str:
"""
一个非纯函数,因为它依赖外部的、会随时间变化的状态(当前时间)。
"""
# 获取当前时间
current_time = datetime.datetime.now() # 获取当前日期和时间
# 打印当前时间和消息,这是副作用
print(f"[{
current_time}] 日志消息: {
message}") # 打印带有时间戳的日志消息
# 返回消息本身
return f"处理完成: {
message}" # 返回处理完成的消息
print(f"\n--- 非纯函数依赖时间问题 ---") # 打印非纯函数依赖时间问题标题
result_time1 = impure_log_time_and_return("首次记录") # 调用函数,输入首次记录消息
result_time2 = impure_log_time_and_return("再次记录") # 再次调用函数,输入再次记录消息
# 尽管输入消息相同,但由于内部依赖时间,每次打印的日志时间戳不同,行为也不同。
# 并且它有打印输出这个副作用。
代码解释:
- 示例 1-1:
pure_add(a: int, b: int) -> int
: 这是一个纯函数。它只接受两个参数a
和b
,并返回它们的和。return a + b
: 函数的唯一作用就是基于输入计算结果并返回,不涉及任何外部状态。result1 = pure_add(5, 3)
和result2 = pure_add(5, 3)
:无论调用多少次,只要输入(5, 3)
不变,输出永远是8
。这就是纯函数的“确定性”。
- 示例 1-2:
global_counter = 0
: 定义了一个全局变量global_counter
。impure_increment_and_add(a: int, b: int) -> int
: 这个函数接收a
和b
。global global_counter
: 声明global_counter
是全局变量,允许函数修改它。global_counter += 1
: 函数修改了global_counter
。return a + b + global_counter
: 函数的输出现在不仅依赖于a
和b
,还依赖于global_counter
的当前值,而global_counter
的值在每次函数调用时都会改变。因此,即使a
和b
相同,函数的输出也会不同,因为它依赖并改变了外部状态,破坏了确定性。
- 示例 1-3:
impure_log_time_and_return(message: str) -> str
: 这个函数获取当前时间datetime.datetime.now()
。print(...)
: 此外,它还执行了打印操作。datetime.datetime.now()
:这是一个外部的、会随时间流逝而变化的“状态”。每次调用,即使message
相同,current_time
也会不同,导致其打印的日志信息不同。这破坏了确定性。同时,print
语句也是一个副作用。
1.2.2 无副作用(No Side Effects)
副作用是指函数在执行过程中对外部环境造成的任何可观察到的影响,除了返回其计算结果之外。避免副作用是函数式编程的核心原则之一,它使得代码更易于理解、测试和并行化。
常见的副作用包括:
- 修改全局变量。
- 修改传入的函数参数(特别是可变对象,如列表、字典)。
- I/O 操作(打印到控制台、读写文件、网络请求)。
- 修改数据库。
- 抛出异常(在某些严格定义中,抛出异常也被视为副作用,但通常在实际FP中可以接受受控的异常)。
- 改变外部系统的状态(例如,发送电子邮件,触发警报)。
# 示例 1-4:纯函数 - 无副作用
# 这是一个纯函数,它不修改任何外部状态或传入参数
def pure_scale_list(numbers: list, factor: float) -> list:
"""
一个纯函数,它接收一个数字列表和一个因子,返回一个新的列表,
其中每个数字都被因子缩放。它不会修改原始列表。
"""
# 使用列表推导式创建一个新列表,而不是修改原始列表
scaled_numbers = [num * factor for num in numbers] # 遍历numbers列表,将每个元素乘以factor,生成一个新的列表
# 返回新列表
return scaled_numbers # 返回新生成的列表
original_list = [1, 2, 3, 4, 5] # 定义一个原始列表
print(f"原始列表: {
original_list}") # 打印原始列表
# 调用纯函数
new_list = pure_scale_list(original_list, 2.0) # 调用pure_scale_list函数,输入原始列表和因子2.0
print(f"缩放后的新列表: {
new_list}") # 打印缩放后的新列表
print(f"原始列表 (未被修改): {
original_list}") # 再次打印原始列表,验证其未被修改
# 验证原始列表未被修改
assert original_list == [1, 2, 3, 4, 5] # 断言原始列表的值没有改变
assert new_list == [2.0, 4.0, 6.0, 8.0, 10.0] # 断言新列表的值符合预期
# 示例 1-5:非纯函数 - 具有修改传入参数的副作用
# 这是一个非纯函数,它修改了传入的列表参数
def impure_append_to_list(data_list: list, item: any) -> list:
"""
一个非纯函数,因为它修改了传入的可变列表参数。
"""
# 将项目追加到传入的列表中
data_list.append(item) # 向传入的列表data_list中添加一个元素item
# 返回修改后的列表
return data_list # 返回修改后的列表
print(f"\n--- 非纯函数修改传入参数的副作用 ---") # 打印非纯函数副作用标题
my_data = [10, 20, 30] # 定义一个列表
print(f"初始列表 my_data: {
my_data}") # 打印初始列表
# 第一次调用非纯函数
returned_list1 = impure_append_to_list(my_data, 40) # 调用impure_append_to_list函数,向my_data添加40
print(f"第一次调用后返回的列表: {
returned_list1}") # 打印第一次调用后返回的列表
print(f"原始列表 my_data (已被修改): {
my_data}") # 打印原始列表,发现它已经被修改
# 第二次调用非纯函数
returned_list2 = impure_append_to_list(my_data, 50) # 再次调用impure_append_to_list函数,向my_data添加50
print(f"第二次调用后返回的列表: {
returned_list2}") # 打印第二次调用后返回的列表
print(f"原始列表 my_data (再次被修改): {
my_data}") # 打印原始列表,发现它再次被修改
# 验证副作用:原始列表已被修改
assert my_data == [10, 20, 30, 40, 50] # 断言my_data列表被修改
assert returned_list1 is my_data # 断言返回的列表和原始列表是同一个对象,证明了原地修改
# 示例 1-6:非纯函数 - 具有I/O副作用
# 这是一个非纯函数,它执行了打印操作(I/O)
def impure_process_with_print(data: str) -> str:
"""
一个非纯函数,因为它执行了I/O操作(打印到控制台)。
"""
# 打印一条处理信息,这是副作用
print(f"正在处理数据: '{
data}'") # 打印处理数据的信息
# 返回处理后的字符串
return data.upper() # 将输入字符串转换为大写并返回
print(f"\n--- 非纯函数I/O副作用 ---") # 打印非纯函数I/O副作用标题
processed_data = impure_process_with_print("hello functional programming") # 调用impure_process_with_print函数
print(f"处理结果: {
processed_data}") # 打印处理结果
代码解释:
- 示例 1-4:
pure_scale_list(numbers: list, factor: float) -> list
: 这是一个纯函数。它接收一个列表numbers
。scaled_numbers = [num * factor for num in numbers]
: 使用列表推导式创建了一个新列表。原始的numbers
列表没有被改变。return scaled_numbers
: 返回这个新列表。original_list
在函数调用前后保持不变,这证明了函数没有修改其参数,也没有其他可观察到的副作用。
- 示例 1-5:
impure_append_to_list(data_list: list, item: any) -> list
: 这是一个非纯函数。它接收一个列表data_list
。data_list.append(item)
: 这是关键。它直接在原地修改了传入的data_list
。在 Python 中,列表是可变对象,通过append
方法修改列表会影响到函数外部的原始列表引用。returned_list1 is my_data
: 这行断言证实了impure_append_to_list
返回的其实就是my_data
这个对象的引用,而不是它的副本。- 这种原地修改的行为,就是副作用。它使得函数的行为变得难以预测,因为函数的输出不仅依赖于输入,还依赖于
data_list
在函数调用前的状态,并且函数本身改变了data_list
的状态。
- 示例 1-6:
impure_process_with_print(data: str) -> str
:print(...)
: 函数内部执行了print
语句。print
操作会将数据发送到标准输出(通常是控制台),这是对外部环境的一种影响,因此是一个副作用。即使函数本身没有改变任何数据结构,它的行为也会影响到外部可观察的环境。
纯函数的重要性总结:
- 可预测性(Predictability):纯函数总是返回相同的结果,这使得它们在任何地方、任何时间都可以安全地调用,而无需担心上下文。
- 可测试性(Testability):由于没有副作用,纯函数的测试变得极其简单。您只需要为给定的输入提供预期的输出,而无需设置复杂的测试环境或清理测试后的状态。
- 并行性(Parallelism):纯函数不共享状态,也不修改外部状态。这意味着它们可以在多线程或多进程环境中并行安全地执行,而无需担心竞态条件或锁。
- 可维护性(Maintainability):纯函数更容易理解,因为您只需要查看其输入和输出,无需跟踪复杂的外部状态变化。这使得代码更易于调试和修改。
- 缓存(Caching):由于确定性,纯函数的计算结果可以被缓存(Memoization)。如果相同的输入再次出现,可以直接返回缓存的结果,而无需重新计算,从而提高性能。
在函数式编程中,我们极力推崇使用纯函数,并尽量将副作用隔离到程序的特定边界。
1.3 不可变性(Immutability):纯函数的基础
不可变性是指一个对象在被创建之后,其内部状态不能被修改。如果需要修改一个不可变对象,唯一的方法是创建一个新的对象来承载修改后的状态。不可变性与纯函数紧密相连,因为纯函数通过不修改其参数来实现无副作用,这正是利用了不可变数据的特性。
Python中的可变与不可变类型:
- 不可变类型(Immutable Types):
- 数字 (
int
,float
,complex
) - 字符串 (
str
) - 元组 (
tuple
) - 冻结集合 (
frozenset
)
- 数字 (
- 可变类型(Mutable Types):
- 列表 (
list
) - 字典 (
dict
) - 集合 (
set
) - 字节数组 (
bytearray
)
- 列表 (
# 示例 1-7:不可变性 - 字符串
# 字符串是不可变类型
my_string = "Hello" # 定义一个字符串
print(f"原始字符串: {
my_string}, ID: {
id(my_string)}") # 打印原始字符串及其内存ID
# 尝试“修改”字符串
# 实际上是创建了一个新字符串并让变量指向它
my_string = my_string + " World" # 对字符串进行拼接操作
print(f"修改后字符串: {
my_string}, ID: {
id(my_string)}") # 打印修改后字符串及其新的内存ID
# 可以看到内存ID发生了变化,证明原字符串未变,而是创建了新字符串
# 示例 1-8:不可变性 - 元组
# 元组是不可变类型
my_tuple = (1, 2, [3, 4]) # 定义一个元组,包含一个列表(可变对象)
print(f"原始元组: {
my_tuple}, ID: {
id(my_tuple)}") # 打印原始元组及其内存ID
# 尝试修改元组的元素 (会报错)
try:
my_tuple[0] = 5 # 尝试修改元组的第一个元素
except TypeError as e:
print(f"尝试修改元组元素失败: {
e}") # 打印修改元组元素失败的错误信息
# 但如果元组中包含可变对象,可变对象本身可以被修改
print(f"修改前元组内的列表: {
my_tuple[2]}, ID: {
id(my_tuple[2])}") # 打印元组内的列表及其内存ID
my_tuple[2].append(5) # 修改元组内的列表
print(f"修改后元组内的列表: {
my_tuple[2]}, ID: {
id(my_tuple[2])}") # 打印修改后元组内的列表及其内存ID
print(f"修改后元组: {
my_tuple}, ID: {
id(my_tuple)}") # 打印修改后元组及其内存ID(元组ID不变)
# 尽管元组本身ID没变,但其内部的可变列表被修改了,这仍然是一种副作用的来源。
# 在严格的函数式编程中,应避免在不可变容器中包含可变对象,或确保不对这些可变对象进行修改。
# 示例 1-9:可变性 - 列表
# 列表是可变类型
my_list = [1, 2, 3] # 定义一个列表
print(f"原始列表: {
my_list}, ID: {
id(my_list)}") # 打印原始列表及其内存ID
# 修改列表 (原地修改)
my_list.append(4) # 向列表中添加一个元素
print(f"修改后列表: {
my_list}, ID: {
id(my_list)}") # 打印修改后列表及其内存ID
# 可以看到内存ID保持不变,证明是原地修改
my_list[0] = 100 # 修改列表的元素
print(f"再次修改后列表: {
my_list}, ID: {
id(my_list)}") # 打印再次修改后列表及其内存ID
# 内存ID不变,依然是原地修改
代码解释:
- 示例 1-7 (字符串):
my_string = "Hello"
:创建一个字符串对象。id(my_string)
:获取该字符串对象的内存地址(ID)。my_string = my_string + " World"
:这行代码并没有修改原有的 “Hello” 字符串对象。实际上,它创建了一个全新的字符串对象 “Hello World”,然后将my_string
变量的引用指向了这个新对象。原来的 “Hello” 对象如果不再被引用,会被垃圾回收。通过id()
验证了内存地址的变化。
- 示例 1-8 (元组):
my_tuple = (1, 2, [3, 4])
: 创建一个元组。元组本身是不可变的,即其包含的元素引用不能改变。my_tuple[0] = 5
:尝试修改元组的元素,会引发TypeError
,因为元组的元素是不可变的。my_tuple[2].append(5)
:关键点。虽然元组本身不可变,但其第三个元素my_tuple[2]
是一个列表[3, 4]
,而列表是可变的。所以,我们可以通过my_tuple[2]
这个引用去修改列表[3, 4]
的内容,使其变为[3, 4, 5]
。这并没有改变元组本身(元组的 ID 不变),但它改变了元组内部引用的某个对象的状态。在严格的函数式编程中,这种行为也是应避免的,因为它引入了可变状态。
- 示例 1-9 (列表):
my_list = [1, 2, 3]
: 创建一个列表对象。my_list.append(4)
和my_list[0] = 100
: 这些操作直接在原地修改了my_list
对象的内容。通过id()
验证了内存地址在修改前后保持不变。
不可变性的重要性:
- 简化并发编程:当数据不可变时,多个线程或进程可以安全地访问相同的数据,因为它们知道数据不会在未经通知的情况下被其他线程修改。这消除了竞态条件和死锁的风险,极大地简化了并行程序的编写。
- 提高可预测性:一旦一个对象被创建,它的状态就永远不会改变。这使得推理程序的行为变得更容易,因为您不需要担心某个函数在某个时刻可能会意外地修改了您依赖的数据。
- 易于缓存:由于不可变对象的状态永远不变,它们的哈希值(如果可哈希)也永远不变。这使得它们非常适合用作字典的键或集合的元素,并且它们的计算结果可以安全地缓存。
- 支持持久化数据结构(Persistent Data Structures):在函数式编程中,当您对数据进行“修改”时,实际上是创建了一个新版本的数据结构,而旧版本的数据结构保持不变。这使得您可以轻松地回溯到数据的历史版本,实现类似版本控制的功能。
在Python的函数式编程实践中,虽然无法完全摆脱可变数据(例如,列表和字典在Python中是核心数据结构),但我们应尽可能地拥抱不可变性,并在必须使用可变数据时,采取防御性编程策略,例如创建副本而非原地修改。标准库中的 collections.namedtuple
和 frozenset
以及第三方库(如 Pyrsistent
、attrs
、dataclasses
结合 frozen=True
)可以帮助我们创建更丰富的不可变数据结构。
1.4 一等函数(First-Class Functions)与高阶函数(Higher-Order Functions)
在函数式编程中,函数不再仅仅是执行任务的工具,它们本身也成为了“数据”——可以像其他任何数据类型一样被对待。这是“一等函数”和“高阶函数”概念的核心。
1.4.1 一等函数(First-Class Functions)
在Python中,函数被视为“一等公民”,这意味着它们具备以下能力:
- 可以被赋值给变量:函数可以像普通变量一样被赋值给另一个变量。
- 可以作为参数传递给其他函数:函数可以作为回调函数或策略传递给高阶函数。
- 可以作为其他函数的返回值:函数可以动态地生成并返回另一个函数。
- 可以存储在数据结构中:函数可以作为列表的元素、字典的值等。
这些特性使得函数在Python中具有极大的灵活性,是实现高阶函数和更抽象编程模式的基础。
# 示例 1-10:一等函数 - 赋值给变量
# 定义一个简单的问候函数
def greet_english(name: str) -> str:
"""用英语问候某人。"""
return f"Hello, {
name}!" # 返回英文问候语
# 将函数赋值给另一个变量
my_greeting_function = greet_english # 将greet_english函数赋值给my_greeting_function变量
# 通过新变量调用函数
print(f"通过变量调用: {
my_greeting_function('Alice')}") # 通过my_greeting_function变量调用函数
# 示例 1-11:一等函数 - 作为参数传递给其他函数 (高阶函数的例子)
# 定义一个高阶函数,它接受一个函数作为参数
def apply_operation(value: any, operation_func) -> any:
"""
一个高阶函数,将传入的值应用到传入的操作函数上。
"""
# 调用传入的函数
return operation_func(value) # 调用operation_func函数,并将value作为参数传递
# 定义一个将数字平方的函数
def square(x: int) -> int:
"""计算一个数的平方。"""
return x * x # 返回x的平方
# 定义一个将字符串转换为大写的函数
def to_uppercase(s: str) -> str:
"""将字符串转换为大写。"""
return s.upper() # 返回字符串的大写形式
# 将函数作为参数传递
print(f"应用平方操作: {
apply_operation(5, square)}") # 调用apply_operation,将square函数作为参数传递
print(f"应用大写操作: {
apply_operation('python', to_uppercase)}") # 调用apply_operation,将to_uppercase函数作为参数传递
# 示例 1-12:一等函数 - 作为其他函数的返回值 (闭包的例子)
# 定义一个外部函数,它返回一个内部函数
def create_multiplier(factor: float):
"""
一个外部函数,返回一个内部函数(闭包)。
内部函数会记住factor的值。
"""
# 定义内部函数
def multiplier(number: float) -> float:
"""根据外部函数的factor来乘以一个数字。"""
# 返回数字乘以factor的结果
return number * factor # 将传入的number与外部函数的factor相乘
# 返回内部函数
return multiplier # 返回内部定义的multiplier函数
# 创建一个乘以2的函数
multiply_by_2 = create_multiplier(2) # 调用create_multiplier函数,创建并返回一个乘以2的函数
# 创建一个乘以5的函数
multiply_by_5 = create_multiplier(5) # 调用create_multiplier函数,创建并返回一个乘以5的函数
# 调用返回的函数
print(f"10 乘以 2: {
multiply_by_2(10)}") # 调用multiply_by_2函数
print(f"10 乘以 5: {
multiply_by_5(10)}") # 调用multiply_by_5函数
# 示例 1-13:一等函数 - 存储在数据结构中
# 将函数存储在列表中
operations = [square, to_uppercase] # 将square和to_uppercase函数存储在列表中
print(f"列表中的函数: {
operations}") # 打印包含函数的列表
# 遍历列表并调用函数
for op in operations: # 遍历operations列表中的每个函数
if op == square: # 如果当前函数是square
print(f"调用 {
op.__name__}: {
op(7)}") # 打印函数名并调用函数
elif op == to_uppercase: # 如果当前函数是to_uppercase
print(f"调用 {
op.__name__}: {
op('data')}") # 打印函数名并调用函数
# 将函数存储在字典中
operation_map = {
'sq': square, # 键'sq'对应square函数
'up': to_uppercase # 键'up'对应to_uppercase函数
} # 定义一个字典,将字符串键映射到函数
print(f"字典中的函数: {
operation_map}") # 打印包含函数的字典
print(f"从字典获取并调用平方函数: {
operation_map['sq'](9)}") # 从字典中获取square函数并调用
代码解释:
- 示例 1-10 (赋值给变量):
my_greeting_function = greet_english
将greet_english
函数的引用赋值给了my_greeting_function
变量。现在可以通过my_greeting_function('Alice')
来调用greet_english
函数。这就像将一个数字或字符串赋值给变量一样简单。 - 示例 1-11 (作为参数):
apply_operation(value: any, operation_func) -> any
: 这个函数接收一个值value
和另一个函数operation_func
作为参数。return operation_func(value)
: 在函数内部,它调用了传入的operation_func
。apply_operation(5, square)
和apply_operation('python', to_uppercase)
:我们展示了如何将square
和to_uppercase
这两个函数作为参数传递给apply_operation
,实现了不同操作的通用执行。这种接受函数作为参数的函数就是高阶函数。
- 示例 1-12 (作为返回值):
create_multiplier(factor: float)
: 这个外部函数定义了一个内部函数multiplier
。return multiplier
: 外部函数返回了这个内部函数。multiply_by_2 = create_multiplier(2)
:当我们调用create_multiplier(2)
时,它会返回一个新的函数multiplier
,并且这个新函数“记住”了它被创建时factor
的值是2
。这种现象被称为闭包(Closure)。print(f"10 乘以 2: {multiply_by_2(10)}")
:现在multiply_by_2
就像一个独立的函数一样被调用,并且它知道要乘以2
。
- 示例 1-13 (存储在数据结构中):
operations = [square, to_uppercase]
: 直接将函数对象作为元素存储在列表中。operation_map = {'sq': square, 'up': to_uppercase}
: 将函数对象作为值存储在字典中。- 这展示了函数可以像普通数据一样,被集合起来、遍历、或通过键值对访问。
1.4.2 高阶函数(Higher-Order Functions)
高阶函数是满足以下至少一个条件的函数:
- 接受一个或多个函数作为参数。
- 返回一个函数作为结果。
在Python中,高阶函数是函数式编程的强大体现。它们允许我们创建抽象、可重用和灵活的代码。Python内置了许多高阶函数(如 map
, filter
, sorted
),同时我们也可以轻松地自定义高阶函数。
# 示例 1-14:高阶函数 - 内置的 map()
# map() 函数接受一个函数和一个可迭代对象作为参数,
# 将函数应用于可迭代对象中的每个元素,并返回一个迭代器。
numbers_for_map = [1, 2, 3, 4, 5] # 定义一个数字列表
# 定义一个将数字翻倍的纯函数
def double(x: int) -> int:
"""将一个整数翻倍。"""
return x * 2 # 返回整数的2倍
# 使用 map() 应用 double 函数
doubled_numbers_iterator = map(double, numbers_for_map) # 使用map函数将double应用于numbers_for_map列表
# map() 返回的是一个迭代器,需要转换为列表才能看到结果
doubled_numbers_list = list(doubled_numbers_iterator) # 将迭代器转换为列表
print(f"使用 map() 翻倍后的列表: {
doubled_numbers_list}") # 打印翻倍后的列表
# 示例 1-15:高阶函数 - 内置的 filter()
# filter() 函数接受一个谓词函数(返回布尔值的函数)和一个可迭代对象,
# 返回一个迭代器,包含使谓词函数返回 True 的元素。
numbers_for_filter = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # 定义一个数字列表
# 定义一个判断数字是否为偶数的谓词函数(纯函数)
def is_even(num: int) -> bool:
"""判断一个整数是否为偶数。"""
return num % 2 == 0 # 判断数字除以2的余数是否为0
# 使用 filter() 过滤偶数
even_numbers_iterator = filter(is_even, numbers_for_filter) # 使用filter函数过滤numbers_for_filter列表中的偶数
even_numbers_list = list(even_numbers_iterator) # 将迭代器转换为列表
print(f"使用 filter() 过滤后的偶数列表: {
even_numbers_list}") # 打印过滤后的偶数列表
# 示例 1-16:高阶函数 - 自定义一个简单的 logger
# 这个高阶函数接受一个消息和一个处理函数,返回一个新的函数,
# 该新函数在调用处理函数之前打印一条日志。
def create_logger(log_message: str):
"""
一个高阶函数,返回一个内部函数(闭包),
用于在调用另一个函数之前打印日志。
"""
# 返回一个接受一个函数作为参数的内部函数
def logger_wrapper(func):
"""
这个内部函数是实际的装饰器,它接受一个函数,
并返回一个新的函数,这个新函数在执行func之前会记录日志。
"""
# 返回一个包装了原始函数的内部函数
def wrapped_func(*args, **kwargs):
"""
这个函数是最终被调用的函数,它会打印日志,然后调用原始函数。
"""
# 打印日志消息
print(f"日志: {
log_message}") # 打印预设的日志消息
# 调用原始函数并返回其结果
return func(*args, **kwargs) # 调用被包装的函数并传递所有参数
# 返回包装后的函数
return wrapped_func # 返回包装后的函数
# 返回logger_wrapper函数
return logger_wrapper # 返回用于装饰函数的logger_wrapper
# 使用自定义 logger
my_log_decorator = create_logger("即将执行重要计算...") # 创建一个日志装饰器,附带消息“即将执行重要计算...”
@my_log_decorator # 使用装饰器语法应用my_log_decorator
def complex_calculation(x: int, y: int) -> int:
"""执行一个复杂的乘法和加法计算。"""
intermediate = x * y # 计算x和y的乘积
return intermediate + 10 # 返回乘积加10的结果
print("\n--- 使用自定义日志高阶函数 ---") # 打印自定义日志高阶函数标题
result_calc = complex_calculation(7, 8) # 调用被装饰的函数
print(f"复杂计算结果: {
result_calc}") # 打印计算结果
# 示例 1-17:高阶函数 - 自定义一个函数合成器 (Function Composition)
# 函数合成是将多个简单函数组合成一个复杂函数的技术。
# 结果函数从右到左依次应用输入。
def compose(*functions):
"""
一个高阶函数,用于函数合成。
它接受任意数量的函数作为参数,并返回一个新的函数,
该新函数将输入从右到左依次传递给这些函数。
"""
# 确保至少有一个函数
if not functions: # 如果没有传入任何函数
# 返回一个恒等函数(原样返回输入)
return lambda x: x # 返回一个lambda函数,它原样返回输入
# 从右侧的函数开始应用
# reduce 用于从左到右应用函数,所以我们需要反转函数顺序并调整逻辑
def composed_function(arg): # 定义组合函数,接受一个参数
# 从最右边的函数开始,将其应用于输入
result = arg # 初始化结果为传入的参数
# 逆序遍历函数列表,从最右边的函数开始
for func in reversed(functions): # 逆序遍历传入的functions
# 将当前结果传递给下一个函数
result = func(result) # 将结果传递给当前函数并更新结果
# 返回最终结果
return result # 返回最终计算结果
# 返回组合后的函数
return composed_function # 返回组合后的函数
# 定义一些简单的函数
def add_one(x: int) -> int:
"""加1。"""
return x + 1 # 返回x加1
def multiply_by_three(x: int) -> int:
"""乘以3。"""
return x * 3 # 返回x乘以3
def to_string(x: any) -> str:
"""转换为字符串。"""
return str(x) # 将输入转换为字符串
# 合成函数:先加1,再乘以3,最后转为字符串
# 注意:compose函数应用顺序是从右到左,所以to_string最先作用在add_one的结果上
# 错误的理解,应该是从左到右。这里是模拟的从右到左,因为reduce通常从左到右
# 实际上,常见的compose是f(g(x))的形式,即从右到左应用
# 这里的实现是:compose(f, g, h)(x) => f(g(h(x)))
# 那么调用时就应该写成 compose(to_string, multiply_by_three, add_one)
combined_function = compose(to_string, multiply_by_three, add_one) # 组合to_string, multiply_by_three, add_one三个函数
print(f"\n--- 函数合成器 ---") # 打印函数合成器标题
# 假设输入是 5:
# add_one(5) = 6
# multiply_by_three(6) = 18
# to_string(18) = "18"
print(f"合成函数 (先加1,再乘3,再转字符串) 应用于 5: {
combined_function(5)}") # 调用组合函数,输入5
代码解释:
- 示例 1-14 (
map
):double(x: int) -> int
: 这是一个纯函数,将数字翻倍。map(double, numbers_for_map)
:map
是一个内置的高阶函数。它接受double
函数(作为参数)和numbers_for_map
列表。map
不会立即计算,而是返回一个迭代器,当你遍历它时,double
函数会被惰性地应用于列表中的每个元素。list(doubled_numbers_iterator)
: 将迭代器转换为列表,显示最终结果。
- 示例 1-15 (
filter
):is_even(num: int) -> bool
: 这是一个谓词函数,返回布尔值。filter(is_even, numbers_for_filter)
:filter
也是一个内置高阶函数。它接受is_even
函数和数字列表。它返回一个迭代器,只包含使is_even
返回True
的元素。
- 示例 1-16 (自定义
create_logger
):create_logger(log_message: str)
: 这是一个高阶函数,因为它返回另一个函数logger_wrapper
。logger_wrapper(func)
: 这个内部函数本身也是一个高阶函数,因为它接受一个函数func
作为参数。wrapped_func(*args, **kwargs)
: 这是最内层的函数,它会执行日志打印(副作用),然后调用原始的func
。@my_log_decorator
: 这是一个装饰器语法糖,本质上等同于complex_calculation = my_log_decorator(complex_calculation)
。这展示了高阶函数在 Python 中强大的应用模式——装饰器。- 这个例子展示了高阶函数如何用于在不修改原始函数逻辑的情况下,为其添加额外的行为(如日志记录)。
- 示例 1-17 (自定义
compose
):compose(*functions)
: 这个高阶函数接受任意数量的函数作为参数。composed_function(arg)
: 它返回一个新的函数。for func in reversed(functions): result = func(result)
: 内部逻辑从右到左(或反转列表后从左到右)依次应用传入的函数。例如,compose(f, g, h)(x)
会计算f(g(h(x)))
。combined_function = compose(to_string, multiply_by_three, add_one)
:创建了一个新的复合函数。combined_function(5)
:当调用这个复合函数时,add_one
会首先应用于5
(得到6
),然后multiply_by_three
应用于6
(得到18
),最后to_string
应用于18
(得到"18"
)。
高阶函数的重要性:
- 代码复用与抽象:通过将通用逻辑(如迭代、过滤、转换、日志记录)封装在高阶函数中,可以避免重复代码,并创建更抽象、更可复用的模块。
- 函数组合:高阶函数使得将多个简单的函数组合成更复杂的函数变得容易,这是一种强大的构建复杂逻辑的方式。
- 声明式编程风格:使用高阶函数通常能够以更声明式的方式表达意图。例如,
map(double, numbers)
比[num * 2 for num in numbers]
更能直接表达“对每个数字进行翻倍操作”。 - 增强灵活性:通过将行为作为参数传递,高阶函数使得函数可以在运行时动态地改变其行为。
一等函数和高阶函数是函数式编程的强大基石,它们使得代码更具表达力、模块化和可测试性。在后续章节中,我们将看到它们在Python函数式编程实践中的广泛应用。
1.5 避免副作用:隔离与模式
虽然纯函数是理想状态,但在实际的程序中,I/O、数据库操作、修改全局配置等副作用是不可避免的。函数式编程并非完全禁止副作用,而是强调要隔离和管理副作用。目标是将程序的绝大部分逻辑编写为纯函数,而将少部分、必要且可控的副作用集中到特定的、明确定义的边界。
1.5.1 明确区分纯逻辑与副作用操作
一个好的函数式程序结构,会清晰地区分两类函数:
- 纯计算函数:这些函数严格遵循纯函数的定义,只依赖输入并产生输出,不与外部世界交互。它们是程序的核心逻辑,易于测试和理解。
- 副作用函数:这些函数负责与外部世界交互(如读文件、写数据库、打印日志)。它们应该被小心地设计和使用,通常是作为程序的“边界”或“入口/出口”。
# 示例 1-18:隔离副作用 - 数据处理流程
import json # 导入json模块,用于处理JSON数据
# --- 纯计算部分 ---
# 纯函数:将列表中的数字平方
def square_numbers(numbers: list) -> list:
"""
纯函数:接收一个数字列表,返回一个新列表,其中每个数字都是原数字的平方。
不修改原始列表,没有副作用。
"""
return [num * num for num in numbers] # 遍历列表,计算每个元素的平方并返回新列表
# 纯函数:过滤出大于阈值的数字
def filter_greater_than_threshold(numbers: list, threshold: int) -> list:
"""
纯函数:接收一个数字列表和阈值,返回一个新列表,包含所有大于阈值的数字。
不修改原始列表,没有副作用。
"""
return [num for num in numbers if num > threshold] # 遍历列表,过滤出大于阈值的数字并返回新列表
# 纯函数:将数据转换为JSON字符串(数据到字符串的转换是纯的)
def convert_to_json_string(data: dict) -> str:
"""
纯函数:接收一个字典,将其转换为JSON格式的字符串。
没有副作用。
"""
return json.dumps(data, indent=2, ensure_ascii=False) # 将字典转换为JSON字符串,indent=2表示缩进2格,ensure_ascii=False表示不转义非ASCII字符
# --- 副作用部分 ---
# 副作用函数:从文件中读取原始数据
def read_numbers_from_file(filepath: str) -> list:
"""
副作用函数:从指定文件路径读取数字列表。
涉及文件I/O,所以是非纯的。
"""
numbers = [] # 初始化空列表用于存储数字
try:
# 以读取模式打开文件
with open(filepath, 'r', encoding='utf-8') as f: # 打开文件进行读取
# 遍历文件的每一行
for line in f: # 遍历文件中的每一行
# 尝试将每一行转换为整数并添加到列表中
numbers.append(int(line.strip())) # 移除行首尾空白字符,转换为整数并添加到列表中
except FileNotFoundError: # 捕获文件未找到错误
print(f"错误: 文件未找到 - {
filepath}") # 打印文件未找到错误信息
return [] # 返回空列表
except ValueError: # 捕获值错误
print(f"错误: 文件 '{
filepath}' 中包含非数字行。") # 打印文件内容错误信息
return [] # 返回空列表
# 返回读取到的数字列表
return numbers # 返回从文件中读取的数字列表
# 副作用函数:将处理结果写入文件
def write_result_to_file(filepath: str, content: str):
"""
副作用函数:将内容写入指定文件路径。
涉及文件I/O,所以是非纯的。
"""
try:
# 以写入模式打开文件
with open(filepath, 'w', encoding='utf-8') as f: # 打开文件进行写入
# 写入内容
f.write(content) # 将内容写入文件
print(f"结果已成功写入文件: {
filepath}") # 打印写入成功信息
except IOError as e: # 捕获I/O错误
print(f"错误: 写入文件 '{
filepath}' 失败 - {
e}") # 打印写入失败错误信息
# 副作用函数:打印日志到控制台
def log_message_to_console(message: str):
"""
副作用函数:将消息打印到控制台。
涉及I/O操作,所以是非纯的。
"""
print(f"[LOG] {
message}") # 打印日志信息
# --- 组合纯函数与副作用函数的主流程 ---
if __name__ == "__main__": # 当脚本作为主程序运行时
# 准备:创建一个模拟输入文件
input_filepath = "input_numbers.txt" # 定义输入文件路径
output_filepath = "output_processed_data.json" # 定义输出文件路径
with open(input_filepath, 'w', encoding='utf-8') as f: # 打开文件进行写入
f.write("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n") # 写入数字,每行一个
log_message_to_console(f"开始处理数据从 {
input_filepath}") # 打印日志,表示开始处理数据
# 第一步:读取数据 (副作用)
raw_numbers = read_numbers_from_file(input_filepath) # 从文件中读取原始数字
log_message_to_console(f"读取到 {
len(raw_numbers)} 个数字。") # 打印日志,表示读取到的数字数量
# 第二步:纯函数链式处理 (无副作用)
# 纯函数链:首先平方,然后过滤
squared_numbers = square_numbers(raw_numbers) # 对原始数字列表进行平方计算
filtered_numbers = filter_greater_than_threshold(squared_numbers, 50) # 过滤出平方后大于50的数字
log_message_to_console(f"经过纯计算后,得到 {
len(filtered_numbers)} 个结果。") # 打印日志,表示纯计算后的结果数量
# 第三步:将结果转换为适合输出的格式 (纯函数)
result_data = {
# 定义结果数据字典
"original_count": len(raw_numbers), # 原始数字数量
"threshold_used": 50, # 使用的阈值
"processed_results": filtered_numbers # 处理后的结果
} # 定义一个字典,包含原始数量、阈值和处理结果
json_output_content = convert_to_json_string(result_data) # 将结果数据转换为JSON字符串
# 第四步:写入结果 (副作用)
write_result_to_file(output_filepath, json_output_content) # 将JSON内容写入输出文件
log_message_to_console(f"数据处理完成,结果写入到 {
output_filepath}") # 打印日志,表示数据处理完成
# 清理文件
# import os
# if os.path.exists(input_filepath): os.remove(input_filepath)
# if os.path.exists(output_filepath): os.remove(output_filepath)
代码解释:
- 纯计算部分:
square_numbers(numbers: list) -> list
: 接受一个列表,返回一个新列表,不修改原始列表。这是一个典型的纯函数。filter_greater_than_threshold(numbers: list, threshold: int) -> list
: 接受一个列表和阈值,返回一个新列表,不修改原始列表。这也是一个纯函数。convert_to_json_string(data: dict) -> str
: 接受一个字典,返回一个 JSON 字符串。它只进行数据格式转换,不涉及外部交互。
- 副作用部分:
read_numbers_from_file(filepath: str) -> list
: 涉及open()
和文件读取,这是典型的 I/O 操作,所以是非纯函数。它还包含了错误处理逻辑,这也是副作用处理的一部分。write_result_to_file(filepath: str, content: str)
: 涉及open()
和文件写入,也是典型的 I/O 操作,是非纯函数。log_message_to_console(message: str)
: 涉及print()
,这是标准输出 I/O,是非纯函数。
- 组合纯函数与副作用函数的主流程 (
if __name__ == "__main__":
):- 这个主流程协调了纯计算和副作用操作。
- 首先通过
read_numbers_from_file
进行 I/O(副作用),获取原始数据。 - 然后将原始数据传递给
square_numbers
和filter_greater_than_threshold
组成的纯函数链。这些纯函数处理数据,但不会对外部产生任何影响,它们只是产生新的数据。 - 接着使用
convert_to_json_string
纯函数将处理后的结果格式化。 - 最后通过
write_result_to_file
进行 I/O(副作用),将最终结果保存。 log_message_to_console
被用于在流程的关键点打印信息,这些日志记录也清晰地表明了副作用的发生位置。
示例分析:
这个示例清晰地展示了函数式编程中管理副作用的核心思想:
- 隔离:将所有的 I/O 操作(读取、写入、打印)封装在独立的函数中,明确地标记它们为非纯函数。
- 纯核:将核心的业务逻辑(计算、转换、过滤)实现为纯函数。这些函数之间可以安全地链式调用,因为它们只接受输入,产生输出,不改变任何外部状态。
- 边界:程序的“边界”是与外部世界交互的地方。数据从外部(文件)流入,经过纯函数的“黑箱”处理,然后通过副作用函数流出到外部(文件、控制台)。
通过这种隔离,纯函数部分的代码更容易推理、测试和重用,而副作用部分则被限制在可控的范围内。当程序出现问题时,你可以首先检查纯函数部分,因为它们的行为是可预测的;如果问题不是出在那里,那么问题可能存在于与外部世界交互的副作用函数中。这种结构大大提高了代码的健壮性和可维护性。
第二章:Python中的核心函数式工具与实践(Part 1)
Python作为一门多范式语言,虽然不强制函数式编程风格,但它提供了丰富的内置函数、标准库模块以及语言特性,使得函数式编程在Python中变得非常自然和高效。本章将聚焦于几个最常用且最核心的函数式工具:匿名函数(lambda)、map()
、filter()
、reduce()
,以及Pythonic的列表推导式和生成器表达式。
2.1 匿名函数:Lambda 表达式的妙用与限制
在函数式编程中,我们经常需要创建一些小型、一次性使用的函数,用于传递给高阶函数(如 map()
, filter()
, sorted()
的 key
参数)。如果为这些简单的函数都定义一个完整的 def
语句,会显得冗长且分散注意力。Python为此提供了匿名函数(Anonymous Functions),也称为 Lambda 表达式。
Lambda 表达式提供了一种简洁的方式来创建小型、单行的函数对象。
Lambda 表达式的基本语法:
lambda arguments: expression
lambda
: 关键字,用于定义匿名函数。arguments
: 函数的参数列表,与def
定义的函数参数类似,可以有多个参数,用逗号分隔。expression
: 一个单一的表达式,其计算结果就是 lambda 函数的返回值。Lambda 函数的主体只能是一个表达式,不能包含复杂的语句(如if/else
语句块、for
循环、赋值语句等)。
2.1.1 Lambda 表达式的基本用法与纯粹性
Lambda 表达式天生倾向于创建纯函数,因为其单行表达式的限制使得引入副作用变得困难(尽管不是不可能)。
# 示例 2-1:Lambda 表达式的基本用法
# 定义一个简单的lambda函数,用于加法
add_lambda = lambda x, y: x + y # 定义一个lambda函数,接受x和y作为参数,返回它们的和
print(f"Lambda 加法 (5, 3): {
add_lambda(5, 3)}") # 调用lambda函数并打印结果
# 定义一个lambda函数,用于判断是否为偶数
is_even_lambda = lambda num: num % 2 == 0 # 定义一个lambda函数,接受num作为参数,判断num是否为偶数
print(f"Lambda 判断偶数 (4): {
is_even_lambda(4)}") # 调用lambda函数并打印结果
print(f"Lambda 判断偶数 (7): {
is_even_lambda(7)}") # 调用lambda函数并打印结果
# 定义一个lambda函数,用于字符串大写转换
to_upper_lambda = lambda s: s.upper() # 定义一个lambda函数,接受s作为参数,返回s的大写形式
print(f"Lambda 大写转换 ('hello'): {
to_upper_lambda('hello')}") # 调用lambda函数并打印结果
# Lambda 表达式的纯粹性:相同的输入,相同的输出,无副作用。
# 例如,add_lambda(5, 3) 永远返回 8,且不改变任何外部状态。
# 这正是函数式编程所推崇的。
代码解释:
add_lambda = lambda x, y: x + y
: 创建了一个匿名函数,它接受两个参数x
和y
,并返回它们的和。这个 lambda 表达式被赋值给了add_lambda
变量,因此可以通过add_lambda(5, 3)
像普通函数一样调用它。is_even_lambda = lambda num: num % 2 == 0
: 创建了一个匿名函数,它接受一个参数num
,并返回一个布尔值,指示num
是否为偶数。to_upper_lambda = lambda s: s.upper()
: 创建了一个匿名函数,它接受一个字符串s
,并返回其大写形式。
这些 lambda 表达式都只依赖于它们的输入参数来计算结果,并且不产生任何外部可见的副作用,完美地符合了纯函数的定义。
2.1.2 Lambda 表达式作为高阶函数的参数
Lambda 表达式最常见的用途是作为参数传递给需要函数对象的高阶函数,例如 map()
, filter()
, sorted()
, max()
, min()
等的 key
参数。它们使得代码更加紧凑和内联。
# 示例 2-2:Lambda 表达式作为高阶函数的参数
# 原始数据列表
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # 定义一个数字列表
words = ['apple', 'Banana', 'cherry', 'Date', 'elderberry'] # 定义一个单词列表
data_points = [(10, 'A'), (5, 'C'), (15, 'B'), (2, 'D')] # 定义一个元组列表
print(f"原始数字列表: {
numbers}") # 打印原始数字列表
print(f"原始单词列表: {
words}") # 打印原始单词列表
print(f"原始数据点列表: {
data_points}") # 打印原始数据点列表
# 场景 1: 配合 map() 进行元素转换
# 使用 lambda 将每个数字平方
squared_numbers = list(map(lambda x: x * x, numbers)) # 使用map函数和lambda表达式计算列表中每个数字的平方
print(f"使用 map() 和 lambda 平方后的数字: {
squared_numbers}") # 打印平方后的数字列表
# 场景 2: 配合 filter() 进行元素过滤
# 使用 lambda 过滤出奇数
odd_numbers = list(filter(lambda num: num % 2 != 0, numbers)) # 使用filter函数和lambda表达式过滤列表中的奇数
print(f"使用 filter() 和 lambda 过滤后的奇数: {
odd_numbers}") # 打印过滤后的奇数列表
# 场景 3: 配合 sorted() 的 key 参数进行自定义排序
# 使用 lambda 按单词长度排序 (升序)
sorted_by_length = sorted(words, key=lambda word: len(word)) # 使用sorted函数和lambda表达式按单词长度对列表进行排序
print(f"使用 sorted() 和 lambda 按长度排序的单词: {
sorted_by_length}") # 打印按长度排序后的单词列表
# 使用 lambda 按元组的第二个元素(字母)排序
sorted_by_char = sorted(data_points, key=lambda item: item[1]) # 使用sorted函数和lambda表达式按元组的第二个元素进行排序
print(f"使用 sorted() 和 lambda 按第二个元素排序的数据点: {
sorted_by_char}") # 打印按第二个元素排序后的数据点列表
# 场景 4: 配合 max() 或 min() 的 key 参数
# 使用 lambda 找出列表中绝对值最大的数字
numbers_abs = [-5, 1, -10, 3, 8] # 定义一个包含正负数的列表
max_abs_number = max(numbers_abs, key=lambda x: abs(x)) # 使用max函数和lambda表达式找出列表中绝对值最大的数字
print(f"使用 max() 和 lambda 找出绝对值最大的数字: {
max_abs_number}") # 打印绝对值最大的数字
代码解释:
map(lambda x: x * x, numbers)
:map
函数将 lambda 表达式lambda x: x * x
应用到numbers
列表的每一个元素上。lambda 函数在这里充当了转换规则。filter(lambda num: num % 2 != 0, numbers)
:filter
函数使用 lambda 表达式lambda num: num % 2 != 0
作为谓词,筛选出numbers
列表中返回True
的元素(即奇数)。sorted(words, key=lambda word: len(word))
:sorted
函数的key
参数接受一个函数,该函数会为列表中的每个元素生成一个用于比较的“键”。这里的 lambda 表达式lambda word: len(word)
为每个单词生成其长度作为排序键。max(numbers_abs, key=lambda x: abs(x))
: 类似地,max
函数使用 lambda 表达式lambda x: abs(x)
来为每个数字生成其绝对值作为比较键,从而找出绝对值最大的数字。
示例分析:
Lambda 表达式作为高阶函数的参数时,极大地提高了代码的简洁性和可读性。它们允许你在需要一个小型函数的地方直接定义它,而无需在代码的其他地方创建具名函数。这种内联定义的方式,使得函数的功能和其使用上下文紧密结合,提高了代码的内聚性。
2.1.3 Lambda 表达式的限制与替代方案
尽管 Lambda 表达式非常方便,但其“单行表达式”的限制也意味着它不能处理所有情况。
限制:
- 仅限单个表达式:Lambda 表达式不能包含语句,例如
if/else
语句块、for
循环、赋值操作 (=
)、return
语句(因为表达式本身就是返回值)等。 - 不适合复杂逻辑:如果函数逻辑需要多步操作、条件分支复杂、或者涉及循环,Lambda 表达式会变得难以阅读甚至无法实现。
- 难以调试:匿名函数没有名字,这使得在堆栈跟踪(stack trace)中识别它们变得困难,从而给调试带来不便。
- 文档缺失:Lambda 表达式不能包含文档字符串(docstring),这使得它们的意图在脱离上下文时难以理解。
替代方案:
当 Lambda 表达式的限制成为障碍时,应果断转向使用传统的具名函数(def
语句)或更Pythonic的推导式。
# 示例 2-3:Lambda 表达式的限制与替代方案
# 原始数据
items = [
{
'name': 'Laptop', 'price': 1200, 'in_stock': True, 'category': 'Electronics'},
{
'name': 'Keyboard', 'price': 75, 'in_stock': False, 'category': 'Electronics'},
{
'name': 'Desk', 'price': 300, 'in_stock': True, 'category': 'Furniture'},
{
'name': 'Monitor', 'price': 450, 'in_stock': True, 'category': 'Electronics'},
{
'name': 'Chair', 'price': 150, 'in_stock': True, 'category': 'Furniture'},
{
'name': 'Mouse', 'price': 25, 'in_stock': False, 'category': 'Electronics'},
] # 定义一个包含字典的列表,模拟商品数据
print(f"原始商品数据: {
items}") # 打印原始商品数据
# 场景 1: Lambda 的条件表达式 (可以,但复杂时难以阅读)
# 需求:如果商品有库存且价格低于100,则标记为“Deal”,否则“Regular”
# 使用三元运算符在lambda中实现条件逻辑
categorized_items_lambda = [
(item['name'], 'Deal' if item['in_stock'] and item['price'] < 100 else 'Regular')
for item in items
] # 遍历items列表,根据条件判断标记商品为“Deal”或“Regular”
print(f"\n使用 Lambda (三元运算) 标记商品: {
categorized_items_lambda}") # 打印标记后的商品列表
# 场景 2: 使用具名函数替代复杂 lambda
# 需求:根据库存状态和价格,返回复杂的商品状态字符串
def get_product_status(item: dict) -> str:
"""
根据商品的库存状态和价格返回其详细状态。
这是纯函数,但逻辑略复杂,不适合lambda。
"""
if not item['in_stock']: # 如果商品没有库存
return "Out of Stock" # 返回“Out of Stock”
elif item['price'] < 50: # 如果商品价格低于50
return "Super Bargain" # 返回“Super Bargain”
elif item['price'] < 200: # 如果商品价格低于200
return "Good Value" # 返回“Good Value”
else: # 其他情况
return "Standard" # 返回“Standard”
# 将具名函数应用于列表 (可以使用 map 或列表推导式)
product_statuses_def = list(map(get_product_status, items)) # 使用map函数和get_product_status函数处理items列表
print(f"使用具名函数标记商品状态 (map): {
product_statuses_def}") # 打印标记后的商品状态列表
# 场景 3: Lambda 尝试执行副作用 (不建议,但可能)
# 尝试使用lambda打印,这仍然是副作用
# 虽然可以写,但违背了纯函数的精神,且调试困难
print("\n--- 尝试使用 Lambda 造成副作用 (不推荐) ---") # 打印不推荐信息
# 例如:打印一个值然后返回它
# processed_with_print = list(map(lambda x: print(f"Processing: {x}") or x.upper(), words))
# print(processed_with_print)
# 更好的做法是将副作用封装在具名函数中,并将其隔离。
# 为了避免输出混乱,这里不执行实际的带有print的lambda
# 场景 4: Lambda 尝试赋值 (直接报错)
# 语法上就不允许
try:
# 尝试在lambda中进行赋值操作,这将导致语法错误
# assign_lambda = lambda x: (y := x + 1, y * 2) # Python 3.8+ 允许海象运算符作为表达式一部分,但仍不能是语句
# assign_lambda = lambda x: x = x + 1 # 传统的赋值语句不允许
# print(assign_lambda(5))
print("Lambda 表达式不能包含赋值语句。") # 打印提示信息
except SyntaxError as e: # 捕获语法错误
print(f"尝试在 Lambda 中赋值导致语法错误: {
e}") # 打印语法错误信息
代码解释:
- 场景 1 (Lambda 的条件表达式):
'Deal' if item['in_stock'] and item['price'] < 100 else 'Regular'
:这是一个 Python 的三元运算符(Conditional Expression),它是一个表达式,而不是语句。因此,它可以在 lambda 表达式内部使用。这说明 lambda 并非完全不能处理条件逻辑,但仅限于单行、表达式形式的条件。
- 场景 2 (具名函数替代):
def get_product_status(item: dict) -> str: ...
: 当逻辑需要多个if/elif/else
分支、或者需要更清晰的命名和文档时,使用传统的def
语句定义的具名函数是更好的选择。它使得代码更易读、易懂、易于维护和调试。
- 场景 3 (Lambda 尝试执行副作用):
- 注释掉的代码
lambda x: print(f"Processing: {x}") or x.upper()
演示了通过“滥用”短路逻辑来在 lambda 中引入副作用(print
)。or x.upper()
确保 lambda 仍然返回一个值。这种做法极不推荐,因为它模糊了函数的纯粹性,使得代码难以理解和调试。副作用应该被明确地隔离和管理。
- 注释掉的代码
- 场景 4 (Lambda 尝试赋值):
- 代码尝试在 lambda 中使用赋值语句 (
x = x + 1
) 或海象运算符 (y := x + 1
)。传统的赋值语句在 lambda 中是不允许的,会直接导致SyntaxError
。即使是海象运算符,也必须是表达式的一部分,不能单独作为 lambda 的主体。这再次强调了 lambda 只能包含一个表达式的限制。
- 代码尝试在 lambda 中使用赋值语句 (
Lambda 表达式的最佳实践总结:
- 简洁明了:当需要一个简单、单行的、不涉及复杂逻辑的函数时,优先考虑 lambda。
- 配合高阶函数:它们是
map()
,filter()
,sorted(key=...)
等高阶函数的理想伴侣。 - 避免复杂性:一旦逻辑开始变得复杂(多个
if/else
、循环、赋值等),立即切换到具名函数。 - 拒绝副作用:尽管技术上可能在 lambda 中引入副作用(如
print
),但强烈建议避免这样做,以保持函数的纯粹性和代码的可预测性。
理解 lambda 表达式的强大之处和局限性,是编写优雅且地道Python函数式代码的关键一步。
2.2 map()
函数:转换的利器与惰性求值
map()
是Python内置的高阶函数之一,它在函数式编程中扮演着“转换”或“映射”的角色。它将一个函数应用于序列(或其他可迭代对象)中的每个元素,并返回一个新的迭代器,其中包含了应用函数后的结果。
map()
的基本语法:
map(function, iterable, ...)
function
: 要应用于每个元素的函数。iterable
: 一个或多个可迭代对象,函数将应用于它们的对应元素。
2.2.1 map()
的工作原理与惰性求值
map()
最重要的特性之一是它的惰性求值(Lazy Evaluation)。这意味着 map()
函数本身并不会立即执行计算并生成所有结果,而是返回一个迭代器(map object)。只有当你真正开始遍历这个迭代器(例如,使用 for
循环、将其转换为 list
或 tuple
时),函数才会被逐个元素地应用和计算。
这种惰性求值对于处理大型数据集至关重要,因为它避免了将所有中间结果一次性加载到内存中,从而节省了大量内存。
# 示例 2-4:`map()` 的基本用法与惰性求值
# 原始数据列表
numbers_for_map_lazy = [1, 2, 3, 4, 5] # 定义一个数字列表
words_for_map_lazy = ['hello', 'world', 'python'] # 定义一个单词列表
print(f"原始数字列表: {
numbers_for_map_lazy}") # 打印原始数字列表
print(f"原始单词列表: {
words_for_map_lazy}") # 打印原始单词列表
# 定义一个纯函数:平方
def square_pure(x: int) -> int:
"""计算一个数的平方,是一个纯函数。"""
print(f"计算 {
x} 的平方...") # 打印计算过程中的提示信息(为了演示惰性求值)
return x * x # 返回x的平方
# 场景 1: 观察 map() 返回迭代器
print("\n--- 场景 1: 观察 map() 返回迭代器 ---") # 打印场景1标题
map_object_square = map(square_pure, numbers_for_map_lazy) # 使用map函数和square_pure函数处理numbers_for_map_lazy列表
print(f"map() 返回的对象: {
map_object_square}") # 打印map函数返回的对象
print(f"此时,`square_pure` 函数还未被调用。") # 提示此时函数还未被调用
# 场景 2: 遍历迭代器触发惰性求值
print("\n--- 场景 2: 遍历迭代器触发惰性求值 ---") # 打印场景2标题
# 只有当开始遍历时,square_pure 才会被调用
for num in map_object_square: # 遍历map对象
print(f"获取到结果: {
num}") # 打印获取到的结果
# 场景 3: 将 map() 结果转换为列表
# 再次调用map,因为迭代器一次性使用
print("\n--- 场景 3: 将 map() 结果转换为列表 ---") # 打印场景3标题
map_object_upper = map(lambda s: s.upper(), words_for_map_lazy) # 使用map函数和lambda表达式将words_for_map_lazy列表中的字符串转换为大写
upper_words_list = list(map_object_upper) # 将map对象转换为列表
print(f"转换为列表: {
upper_words_list}") # 打印转换后的列表
# 场景 4: map() 处理多个可迭代对象 (对应位置元素)
print("\n--- 场景 4: map() 处理多个可迭代对象 ---") # 打印场景4标题
list_a = [1, 2, 3] # 定义列表a
list_b = [10, 20, 30] # 定义列表b
list_c = [100, 200, 300] # 定义列表c
# 定义一个纯函数:三数相加
def add_three_numbers(x, y, z): # 定义一个纯函数,用于计算三个数的和
"""纯函数:计算三个数的和。"""
return x + y + z # 返回x, y, z的和
# 使用 map() 结合三个列表
sums_of_three_lists = list(map(add_three_numbers, list_a, list_b, list_c)) # 使用map函数和add_three_numbers函数处理三个列表
print(f"三个列表对应元素求和: {
sums_of_three_lists}") # 打印对应元素求和后的列表
# 注意:如果可迭代对象长度不一,map 会以最短的那个为准停止。
list_short = [1, 2] # 定义一个短列表
list_long = [10, 20, 30, 40] # 定义一个长列表
combined_short_long = list(map(lambda x, y: x * y, list_short, list_long)) # 使用map函数和lambda表达式处理短列表和长列表
print(f"短列表与长列表结合 (以最短为准): {
combined_short_long}") # 打印结合后的列表
代码解释:
- 场景 1 (惰性求值):
map_object_square = map(square_pure, numbers_for_map_lazy)
: 当我们调用map()
时,它立即返回一个map
对象。此时,square_pure
函数还未被执行。打印map_object_square
会显示它的内存地址,而不是计算结果。
- 场景 2 (遍历触发计算):
for num in map_object_square:
: 当我们开始遍历map_object_square
时,square_pure
函数会逐个元素地被调用,每次计算一个元素的平方,然后将结果返回。这从控制台的打印输出 (计算 X 的平方...
) 可以清晰地观察到。这种按需计算的模式就是惰性求值。
- 场景 3 (转换为列表):
upper_words_list = list(map_object_upper)
: 将map
对象强制转换为list
,这会立即触发所有元素的计算。
- 场景 4 (多个可迭代对象):
map(add_three_numbers, list_a, list_b, list_c)
:map
可以接受多个可迭代对象。在这种情况下,function
必须能够接受与可迭代对象数量相同的参数。map
会从每个可迭代对象中取出对应位置的元素,将它们作为参数传递给function
。map
会在最短的可迭代对象耗尽时停止。
示例分析:
map()
在函数式编程中是进行一对一元素转换(或多对一转换)的理想工具。
- 优点:
- 简洁:将函数应用到整个序列的代码非常紧凑。
- 惰性求值:对于大型数据集,
map()
避免了不必要的内存消耗,因为它只在需要时计算结果。 - 可组合性:
map()
的输出是迭代器,可以方便地与其他迭代器工具(如filter()
或其他map()
链)组合,形成数据处理管道。
- 缺点:
- 可读性不如列表推导式:对于简单的转换,列表推导式(下一节会详细介绍)通常被认为是更 Pythonic 和易读的。
- 错误处理:如果
function
在处理某个元素时抛出异常,整个map
迭代器会停止。
2.2.2 map()
与列表推导式(List Comprehensions)的对比
对于大多数简单的元素级转换,Python 的**列表推导式(List Comprehensions)**是比 map()
更常用和推荐的替代方案。
列表推导式的基本语法:
[expression for item in iterable if condition]
尽管列表推导式通常不是惰性求值的(它会立即构建整个列表),但在代码的简洁性、可读性和Pythonic风格方面,它通常更胜一筹。
# 示例 2-5:`map()` 与列表推导式的对比
numbers_compare = [1, 2, 3, 4, 5, 6] # 定义一个用于比较的数字列表
print(f"原始数字列表: {
numbers_compare}") # 打印原始数字列表
# 场景 1: 简单的平方转换
print("\n--- 场景 1: 简单的平方转换 ---") # 打印场景1标题
# 使用 map()
map_squared = list(map(lambda x: x * x, numbers_compare)) # 使用map函数和lambda表达式计算每个数字的平方
print(f"map() 平方结果: {
map_squared}") # 打印平方结果
# 使用列表推导式
list_comp_squared = [x * x for x in numbers_compare] # 使用列表推导式计算每个数字的平方
print(f"列表推导式平方结果: {
list_comp_squared}") # 打印平方结果
# 示例分析:对于这种简单场景,列表推导式通常被认为更具可读性。
# 场景 2: 带有条件的转换
print("\n--- 场景 2: 带有条件的转换 (只平方偶数) ---") # 打印场景2标题
# 使用 map() 和 filter() 组合 (更复杂)
map_filter_squared_even = list(map(lambda x: x * x, filter(lambda num: num % 2 == 0, numbers_compare))) # 使用map和filter组合,先过滤偶数再平方
print(f"map() + filter() 偶数平方结果: {
map_filter_squared_even}") # 打印偶数平方结果
# 使用列表推导式 (更简洁和直观)
list_comp_squared_even = [x * x for x in numbers_compare if x % 2 == 0] # 使用列表推导式过滤偶数并平方
print(f"列表推导式偶数平方结果: {
list_comp_squared_even}") # 打印偶数平方结果
# 示例分析:当引入条件时,列表推导式将过滤和转换逻辑紧密结合,可读性优势更明显。
# 场景 3: 性能对比 (对于大数据集)
print("\n--- 场景 3: 性能对比 (对于大数据集) ---") # 打印场景3标题
large_numbers = list(range(10_000_000)) # 创建一个包含1000万个数字的大列表
start_time_map = time.time() # 记录开始时间
# map 默认返回迭代器,所以转换为列表的耗时也包括了计算时间
map_result = list(map(lambda x: x + 1, large_numbers)) # 使用map函数对大列表进行加1操作
end_time_map = time.time() # 记录结束时间
print(f"map() (转换为列表) 耗时: {
end_time_map - start_time_map:.4f} 秒") # 打印map操作耗时
start_time_list_comp = time.time() # 记录开始时间
list_comp_result = [x + 1 for x in large_numbers] # 使用列表推导式对大列表进行加1操作
end_time_list_comp = time.time() # 记录结束时间
print(f"列表推导式耗时: {
end_time_list_comp - start_time_list_comp:.4f} 秒") # 打印列表推导式耗时
# 示例分析:在实际性能上,对于简单操作,列表推导式通常比 map() 更快,
# 因为列表推导式在 Python 解释器级别进行了优化。
# 但 map() 的优势在于惰性求值,当你不需要立即得到所有结果时。
# 如果不立即转换为列表,map的惰性优势就体现出来了,例如:
start_time_map_lazy = time.time() # 记录开始时间
map_lazy_obj = map(lambda x: x + 1, large_numbers) # 使用map函数对大列表进行加1操作,不立即转换为列表
end_time_map_lazy = time.time() # 记录结束时间
print(f"map() (惰性对象创建) 耗时: {
end_time_map_lazy - start_time_map_lazy:.4f} 秒 (极快)") # 打印map惰性对象创建耗时
# 如果你想利用惰性求值但又想保持列表推导式的语法,可以使用生成器表达式 (下一节介绍)
代码解释:
- 场景 1:演示了
map()
和列表推导式在执行简单转换时的语法对比。通常认为[x * x for x in numbers_compare]
更直观。 - 场景 2:演示了在需要同时进行过滤和转换时,列表推导式的优势。
map()
需要与filter()
组合,代码链条更长。 - 场景 3 (性能对比):
- 对于大型列表的简单元素操作,将
map
结果立即转换为list
的性能与列表推导式相当接近,甚至列表推导式在很多情况下会略快,因为它在 C 语言层面进行了更底层的优化。 - 真正的区别在于惰性求值。
map_lazy_obj = map(lambda x: x + 1, large_numbers)
这行代码几乎是瞬时完成的,因为它只是创建了一个迭代器,并没有执行任何实际的计算。只有当迭代器被消耗时(例如通过list()
转换或for
循环),计算才会发生。
- 对于大型列表的简单元素操作,将
map()
的适用场景总结:
- 函数式风格的代码偏好:如果你倾向于严格的函数式编程风格,将转换逻辑明确地封装在一个函数中并传递给
map()
。 - 处理非常大的数据集:当数据集大到无法一次性加载到内存中时,
map()
的惰性求值特性可以节省大量内存。 - 与迭代器管道结合:
map()
返回迭代器,可以很自然地与其他返回迭代器的函数(如filter()
、itertools
模块中的函数)形成高效的数据处理管道。 - 需要同时处理多个可迭代对象:
map(function, iterable1, iterable2, ...)
的用法是列表推导式无法直接替代的。
2.3 filter()
函数:筛选数据的艺术与惰性求值
filter()
是另一个Python内置的高阶函数,它在函数式编程中扮演着“筛选”或“过滤”的角色。它接受一个谓词函数(predicate function,即返回布尔值的函数)和一个可迭代对象,并返回一个新的迭代器,其中只包含使谓词函数返回 True
的元素。
filter()
的基本语法:
filter(predicate, iterable)
predicate
: 一个函数,接受一个元素作为参数,并返回True
或False
。如果为None
,则filter
会移除那些在布尔上下文中为False
的元素(例如,0,None
,''
,[]
,{}
等)。iterable
: 要过滤的可迭代对象。
2.3.1 filter()
的工作原理与惰性求值
与 map()
类似,filter()
也遵循惰性求值原则。它并不会立即遍历所有元素并进行过滤,而是返回一个迭代器(filter object)。只有当你遍历这个迭代器时,谓词函数才会被逐个元素地应用,并根据结果决定是否包含该元素。
# 示例 2-6:`filter()` 的基本用法与惰性求值
numbers_for_filter_lazy = list(range(1, 11)) # 定义一个从1到10的数字列表
words_for_filter_lazy = ['apple', '', 'banana', 'grape', None, 'kiwi'] # 定义一个单词列表,包含空字符串和None
print(f"原始数字列表: {
numbers_for_filter_lazy}") # 打印原始数字列表
print(f"原始单词列表: {
words_for_filter_lazy}") # 打印原始单词列表
# 定义一个纯谓词函数:判断数字是否大于5
def is_greater_than_five(num: int) -> bool:
"""纯谓词函数:判断一个数字是否大于5。"""
print(f"检查数字 {
num} 是否大于5...") # 打印检查过程中的提示信息(为了演示惰性求值)
return num > 5 # 返回num是否大于5的布尔值
# 场景 1: 观察 filter() 返回迭代器
print("\n--- 场景 1: 观察 filter() 返回迭代器 ---") # 打印场景1标题
filter_object_gt5 = filter(is_greater_than_five, numbers_for_filter_lazy) # 使用filter函数和is_greater_than_five函数处理numbers_for_filter_lazy列表
print(f"filter() 返回的对象: {
filter_object_gt5}") # 打印filter函数返回的对象
print(f"此时,`is_greater_than_five` 函数还未被调用。") # 提示此时函数还未被调用
# 场景 2: 遍历迭代器触发惰性求值
print("\n--- 场景 2: 遍历迭代器触发惰性求值 ---") # 打印场景2标题
# 只有当开始遍历时,is_greater_than_five 才会被调用
for num in filter_object_gt5: # 遍历filter对象
print(f"获取到过滤后的结果: {
num}") # 打印获取到的过滤后结果
# 场景 3: 将 filter() 结果转换为列表
print("\n--- 场景 3: 将 filter() 结果转换为列表 ---") # 打印场景3标题
# 再次调用filter,因为迭代器一次性使用
filter_object_even = filter(lambda num: num % 2 == 0, numbers_for_filter_lazy) # 使用filter函数和lambda表达式过滤偶数
even_numbers_list = list(filter_object_even) # 将filter对象转换为列表
print(f"转换为列表 (偶数): {
even_numbers_list}") # 打印转换后的列表
# 场景 4: predicate 为 None - 移除布尔上下文为 False 的元素
print("\n--- 场景 4: predicate 为 None ---") # 打印场景4标题
filtered_non_falsey = list(filter(None, words_for_filter_lazy)) # 使用filter函数,将predicate设置为None,过滤掉布尔上下文为False的元素
print(f"过滤掉布尔上下文为 False 的元素: {
filtered_non_falsey}") # 打印过滤后的列表
代码解释:
- 场景 1 (惰性求值):
filter_object_gt5 = filter(is_greater_than_five, numbers_for_filter_lazy)
: 类似map()
,filter()
立即返回一个filter
对象,此时谓词函数is_greater_than_five
还未被执行。
- 场景 2 (遍历触发计算):
for num in filter_object_gt5:
: 开始遍历filter
对象时,is_greater_than_five
函数会逐个元素地被调用。只有当is_greater_than_five
返回True
时,该元素才会被包含在迭代结果中。
- 场景 3 (转换为列表):
even_numbers_list = list(filter_object_even)
: 将filter
对象强制转换为list
,立即触发所有元素的过滤和计算。
- 场景 4 (predicate 为
None
):filter(None, words_for_filter_lazy)
: 当predicate
参数为None
时,filter
会移除那些在布尔上下文中被视为False
的元素。在 Python 中,0
,None
,False
, 空字符串''
, 空列表[]
, 空字典{}
, 空元组()
等都被视为False
。因此,''
和None
会被移除。
示例分析:
filter()
是在函数式编程中用于根据条件筛选数据的核心工具。
- 优点:
- 简洁:以声明式的方式表达过滤逻辑。
- 惰性求值:对于大型数据集,
filter()
避免了不必要的内存消耗,只保留符合条件的元素。 - 可组合性:
filter()
的输出是迭代器,可以方便地与其他迭代器工具组成处理管道。
- 缺点:
- 可读性不如列表推导式:对于简单的过滤,列表推导式通常更 Pythonic。
- 只能过滤:
filter()
只能根据谓词移除元素,不能对元素进行转换。如果需要转换,通常需要与map()
结合使用。
2.3.2 filter()
与列表推导式(List Comprehensions)的对比
列表推导式在同时进行过滤和转换时通常表现出更好的可读性。
# 示例 2-7:`filter()` 与列表推导式的对比
numbers_compare_filter = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # 定义一个用于比较的数字列表
print(f"原始数字列表: {
numbers_compare_filter}") # 打印原始数字列表
# 场景 1: 简单的偶数过滤
print("\n--- 场景 1: 简单的偶数过滤 ---") # 打印场景1标题
# 使用 filter()
filter_even_result = list(filter(lambda num: num % 2 == 0, numbers_compare_filter)) # 使用filter函数和lambda表达式过滤偶数
print(f"filter() 偶数结果: {
filter_even_result}") # 打印偶数结果
# 使用列表推导式
list_comp_even_result = [num for num in numbers_compare_filter if num % 2 == 0] # 使用列表推导式过滤偶数
print(f"列表推导式偶数结果: {
list_comp_even_result}") # 打印偶数结果
# 示例分析:对于这种简单过滤场景,两者都简洁,但列表推导式在Python社区中更常用。
# 场景 2: 过滤并转换 (例如,只保留偶数并将其平方)
print("\n--- 场景 2: 过滤并转换 (保留偶数并平方) ---") # 打印场景2标题
# 使用 filter() 和 map() 组合 (链式调用)
filter_map_result = list(map(lambda x: x * x, filter(lambda num: num % 2 == 0, numbers_compare_filter))) # 使用filter和map组合,先过滤偶数再平方
print(f"filter() + map() 偶数平方结果: {
filter_map_result}") # 打印偶数平方结果
# 使用列表推导式 (更简洁和直观)
list_comp_filter_map_result = [num * num for num in numbers_compare_filter if num % 2 == 0] # 使用列表推导式过滤偶数并平方
print(f"列表推导式偶数平方结果: {
list_comp_filter_map_result}") # 打印偶数平方结果
# 示例分析:当需要同时过滤和转换时,列表推导式的语法明显更紧凑、更易读。
# 场景 3: 性能对比 (对于大数据集)
print("\n--- 场景 3: 性能对比 (对于大数据集) ---") # 打印场景3标题
large_numbers_filter = list(range(10_000_000)) # 创建一个包含1000万个数字的大列表
start_time_filter = time.time() # 记录开始时间
filter_result = list(filter(lambda x: x % 7 == 0, large_numbers_filter)) # 使用filter函数过滤能被7整除的数字
end_time_filter = time.time() # 记录结束时间
print(f"filter() (转换为列表) 耗时: {
end_time_filter - start_time_filter:.4f} 秒") # 打印filter操作耗时
start_time_list_comp_filter = time.time() # 记录开始时间
list_comp_filter_result = [x for x in large_numbers_filter if x % 7 == 0] # 使用列表推导式过滤能被7整除的数字
end_time_list_comp_filter = time.time() # 记录结束时间
print(f"列表推导式耗时: {
end_time_list_comp_filter - start_time_list_comp_filter:.4f} 秒") # 打印列表推导式耗时
# 示例分析:在实际性能上,列表推导式通常也比 filter() 转换为列表的组合更快。
# 同样,filter() 的主要优势在于惰性求值。
代码解释:
- 场景 1 & 2:演示了
filter()
和列表推导式在执行简单过滤以及过滤+转换时的语法对比。列表推导式的优势再次显现。 - 场景 3 (性能对比):与
map()
类似,对于立即转换为列表的场景,列表推导式在性能上通常优于filter()
。
filter()
的适用场景总结:
- 函数式风格的代码偏好:如果你倾向于严格的函数式编程风格,将谓词逻辑明确地封装在一个函数中并传递给
filter()
。 - 处理非常大的数据集:当数据集大到无法一次性加载到内存中时,
filter()
的惰性求值特性可以节省大量内存。 - 与迭代器管道结合:
filter()
返回迭代器,可以与map()
或itertools
模块中的函数等组成高效的数据处理管道。 - 移除布尔上下文为 False 的元素:当
predicate
为None
时,filter()
提供了一个非常简洁的方式来清理数据中的“假值”元素。
关于 map()
和 filter()
的核心思想:
这两种函数都体现了函数式编程中的核心理念:将“做什么”与“如何做”分离。你定义一个纯粹的函数来描述“做什么样的转换”或“做什么样的筛选”,然后将这个函数作为数据处理的“策略”,传递给 map()
或 filter()
这样的高阶函数。这样,数据处理流程变得更加抽象和通用。
2.4 reduce()
函数:聚合的强大力量
reduce()
函数(在 Python 3 中位于 functools
模块中)是一个强大的函数式工具,用于对列表(或其他可迭代对象)中的元素进行累积计算。它通过重复地将一个二元函数应用于序列的元素,将序列“归约”为单个结果值。
reduce()
的基本语法:
functools.reduce(function, iterable[, initializer])
function
: 一个二元函数,它接受两个参数,并返回一个值。这个函数会被连续地应用于可迭代对象的元素。iterable
: 要归约的可迭代对象。initializer
(可选): 一个初始值。如果提供了initializer
,它将作为function
的第一个参数用于第一次计算;如果没有提供,则可迭代对象的第一个元素将作为第一次计算的第一个参数,并且从第二个元素开始迭代。
2.4.1 reduce()
的工作原理
reduce()
的工作方式可以想象成一个“折叠”或“累积”过程:
- 如果提供了
initializer
:result = initializer
- 对于
iterable
中的每个元素x
:result = function(result, x)
- 如果没有提供
initializer
:result = iterable[0]
(或迭代器的第一个元素)- 对于
iterable
中的剩余元素x
:result = function(result, x)
最终返回 result
。
# 示例 2-8:`reduce()` 的基本用法
from functools import reduce # 从functools模块导入reduce函数
numbers_for_reduce = [1, 2, 3, 4, 5] # 定义一个数字列表
print(f"原始数字列表: {
numbers_for_reduce}") # 打印原始数字列表
# 场景 1: 求和 (不带 initializer)
print("\n--- 场景 1: 求和 (不带 initializer) ---") # 打印场景1标题
# 过程: add(1, 2) -> 3; add(3, 3) -> 6; add(6, 4) -> 10; add(10, 5) -> 15
sum_result = reduce(lambda x, y: x + y, numbers_for_reduce) # 使用reduce函数和lambda表达式对列表进行求和
print(f"使用 reduce() 求和: {
sum_result}") # 打印求和结果
# 场景 2: 求乘积 (不带 initializer)
print("\n--- 场景 2: 求乘积 (不带 initializer) ---") # 打印场景2标题
# 过程: mul(1, 2) -> 2; mul(2, 3) -> 6; mul(6, 4) -> 24; mul(24, 5) -> 120
product_result = reduce(lambda x, y: x * y, numbers_for_reduce) # 使用reduce函数和lambda表达式对列表进行求乘积
print(