23. 一等函数与函数式技巧
在前面的章节中,我们学习了如何定义和调用函数(function)、如何使用形参(parameters)与实参(arguments),以及理解变量作用域(variable scope)。现在我们将探索一个让 Python 与众不同的强大特性:函数是一等对象(first-class objects)。这意味着函数可以像其他任何值一样被对待——存储在变量中、作为参数传递给其他函数,并且从函数中作为返回值返回。
这种能力开启了优雅的编程技巧,让代码更灵活、更可复用、更具表达力。我们将通过实际示例来探索如何利用一等函数,理解闭包(closure)(会“记住”其环境的函数),使用 lambda 表达式(lambda expressions)来简洁地定义函数,并应用 map()、filter()、any()、all() 等内置函数高效地处理集合。
23.1) 函数作为一等对象
23.1.1) “一等”是什么意思
在 Python 中,函数是一等对象(first-class objects),这意味着它们可以:
- 被赋值给变量
- 存储在数据结构中(列表(list)、字典(dictionary)等)
- 作为参数传递给其他函数
- 作为值从其他函数返回
这与某些编程语言不同,在那些语言中函数具有特殊地位,不能像普通值一样被操作。在 Python 中,函数只是另一种对象类型,类似于整数、字符串或列表。
让我们看看实际效果:
# 定义一个简单函数
def greet(name):
return f"Hello, {name}!"
# 将函数赋值给一个变量
say_hello = greet
# 通过新变量调用该函数
message = say_hello("Alice")
print(message) # Output: Hello, Alice!
# 检查两个名字是否引用同一个函数
print(greet) # Output: <function greet at 0x...>
print(say_hello) # Output: <function greet at 0x...>
print(greet is say_hello) # Output: True注意,当我们写 say_hello = greet 时,我们并没有调用函数(没有括号)。我们是在创建一个新名称,它引用同一个函数对象。greet 和 say_hello 现在都指向同一个函数,我们可以使用 is 运算符来验证这一点。
23.1.2) 在数据结构中存储函数
由于函数是对象,我们可以把它们存储在列表、字典或任何其他集合中:
# 将运算操作存储在字典中的计算器
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x, y):
return x * y
def divide(x, y):
return x / y
# 将函数存储在字典中
operations = {
'+': add,
'-': subtract,
'*': multiply,
'/': divide
}
# 使用字典执行计算
num1 = 10
num2 = 5
operator = '*'
result = operations[operator](num1, num2)
print(f"{num1} {operator} {num2} = {result}") # Output: 10 * 5 = 50这种模式对于构建灵活的系统非常有用。与其编写很长的 if-elif 语句链来选择要调用哪个函数,不如在字典中查找合适的函数并直接调用它。
23.2) 将函数作为参数传递
23.2.1) 基本概念
一等函数最强大的用途之一,就是把函数作为参数传递给其他函数。这让我们能够编写灵活、可复用的代码,使其可以适配不同的行为。
下面是一个简单示例:
# 将另一个函数应用到某个值上的函数
def apply_operation(value, operation):
"""将作为参数接收的 operation 函数应用到 value 上。"""
return operation(value)
# 不同的操作
def double(x):
return x * 2
def square(x):
return x * x
def negate(x):
return -x
# 使用同一个 apply_operation 函数搭配不同的操作
number = 5
print(apply_operation(number, double)) # Output: 10
print(apply_operation(number, square)) # Output: 25
print(apply_operation(number, negate)) # Output: -5apply_operation 函数并不知道、也不在意它正在执行什么具体操作。它只是调用传给它的那个函数。这种关注点分离让代码更模块化,也更容易扩展。
23.2.2) 使用自定义函数处理集合
一种常见模式是:使用作为参数传入的函数来处理集合中的每个元素:
# 使用给定函数处理列表中的每个元素
def process_list(items, processor):
"""将 processor 函数应用到列表中的每个元素。"""
results = []
for item in items:
results.append(processor(item))
return results
# 不同的处理函数
def uppercase(text):
return text.upper()
def add_exclamation(text):
return text + "!"
def get_length(text):
return len(text)
# 用不同方式处理同一个列表
words = ["hello", "world", "python"]
print(process_list(words, uppercase)) # Output: ['HELLO', 'WORLD', 'PYTHON']
print(process_list(words, add_exclamation)) # Output: ['hello!', 'world!', 'python!']
print(process_list(words, get_length)) # Output: [5, 5, 6]这种模式非常有用,以至于 Python 提供了 map() 和 filter() 等内置函数来完成这类工作(我们会在 23.6 节中探讨它们)。
23.2.3) 通过提供 key 函数进行排序(简要介绍)
Python 的 sorted() 函数接受一个 key 参数——一个用于决定如何比较元素的函数:
# 按不同条件对学生排序
students = [
{"name": "Alice", "grade": 85, "age": 20},
{"name": "Bob", "grade": 92, "age": 19},
{"name": "Charlie", "grade": 78, "age": 21},
{"name": "Diana", "grade": 95, "age": 20}
]
# 提取成绩的函数
def get_grade(student):
return student["grade"]
# 提取姓名的函数
def get_name(student):
return student["name"]
# 按成绩排序(升序)
by_grade = sorted(students, key=get_grade)
print("Sorted by grade:")
for student in by_grade:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95
# 按姓名排序(按字母顺序)
by_name = sorted(students, key=get_name)
print("\nSorted by name:")
for student in by_name:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Bob: 92
# Charlie: 78
# Diana: 95key 函数会对每个元素调用一次,它的返回值会用于比较。这比必须编写自定义排序逻辑要灵活得多。
这种通过传入函数来自定义行为的模式在 Python 中极其常见。我们将在第 38 章探索更高级的排序技巧。
23.3) 从函数返回函数
23.3.1) 创建函数的函数
正如我们可以把函数作为参数传递一样,我们也可以从其他函数返回函数。这让我们能够动态创建专用函数:
# 创建并返回一个新函数的函数
def create_multiplier(factor):
"""创建一个将输入乘以给定因子的函数。"""
def multiplier(x):
return x * factor
return multiplier
# 创建专用的乘法函数
double = create_multiplier(2)
triple = create_multiplier(3)
times_ten = create_multiplier(10)
# 使用创建出来的函数
print(double(5)) # Output: 10
print(triple(5)) # Output: 15
print(times_ten(5)) # Output: 50这里发生了什么?create_multiplier 函数定义了一个名为 multiplier 的内部函数并返回它。每次我们用不同的因子调用 create_multiplier,都会得到一个新函数,它会“记住”那个特定因子。这是我们第一次直观地看到闭包(closures),我们会在下一节深入探讨。
23.3.2) 创建自定义验证器
返回函数对于创建自定义验证或处理函数尤其有用:
# 动态创建范围验证器
def create_range_validator(min_value, max_value):
"""创建一个函数,用于验证数字是否在指定范围内。"""
def validator(number):
return min_value <= number <= max_value
return validator
# 创建特定验证器
is_valid_age = create_range_validator(0, 120)
is_valid_percentage = create_range_validator(0, 100)
is_room_temperature = create_range_validator(15, 30)
# 使用验证器
age = 25
print(f"Is {age} a valid age? {is_valid_age(age)}") # Output: True
temp = 22
print(f"Is {temp}°C room temperature? {is_room_temperature(temp)}") # Output: True
score = 150
print(f"Is {score} a valid percentage? {is_valid_percentage(score)}") # Output: False23.4) 理解闭包:会记住的函数
23.4.1) 什么是闭包?
闭包(closure) 是一种函数:它会“记住”创建它时所在作用域中的变量,即使那个作用域已经执行结束。在 23.3 节的示例中,我们其实已经在使用闭包,只是没有显式地称它们为闭包。
让我们看看闭包是如何工作的:
def create_counter(start=0):
"""创建一个能记住计数值的计数器函数。"""
count = start # 这个变量被闭包“捕获”
def counter():
nonlocal count # 访问被捕获的变量
count += 1
return count
return counter
# 创建两个互不影响的计数器
counter1 = create_counter(0)
counter2 = create_counter(100)
# 每个计数器维护自己的计数值
print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter1()) # Output: 3
print(counter2()) # Output: 101
print(counter2()) # Output: 102
print(counter1()) # Output: 4 (counter1 is independent of counter2)内部的 counter 函数对 count 变量形成了一个闭包。即使 create_counter 已经执行完毕,返回的 counter 函数仍然可以访问 count。每次调用 create_counter 都会创建一个新的、互相独立的闭包,并拥有各自的 count 变量。
23.4.2) 闭包如何捕获变量
当一个函数在另一个函数内部定义时,它可以访问外部函数作用域中的变量。这些变量会被“捕获”,即使外部函数返回后仍然可访问:
当 Python 创建内部函数时,它不仅会保存函数代码——还会保存内部函数所使用的外部函数变量的引用。这个过程称为“捕获(capturing)”变量。
def create_greeter(greeting):
"""创建一个使用自定义问候语的问候函数。"""
def greet(name):
return f"{greeting}, {name}!"
return greet
# 创建不同的问候函数
say_hello = create_greeter("Hello")
say_hi = create_greeter("Hi")
say_bonjour = create_greeter("Bonjour")
# 每个问候函数都会记住自己特定的问候语
print(say_hello("Alice")) # Output: Hello, Alice!
print(say_hi("Bob")) # Output: Hi, Bob!
print(say_bonjour("Claire")) # Output: Bonjour, Claire!greeting 参数会被闭包捕获。每个问候函数都有自己捕获的 greeting 值,并在每次调用时使用它。
23.4.3) 实用场景:配置型函数
闭包非常适合用来创建具有预配置行为的函数:
# 创建具有不同税率的价格计算器
def create_price_calculator(tax_rate):
"""创建一个应用特定税率的计算器。"""
def calculate_total(price):
tax = price * tax_rate
return price + tax
return calculate_total
# 为不同地区创建计算器
us_calculator = create_price_calculator(0.07) # 7% tax
uk_calculator = create_price_calculator(0.20) # 20% VAT
japan_calculator = create_price_calculator(0.10) # 10% consumption tax
# 计算不同地区的价格
item_price = 100
print(f"US total: ${us_calculator(item_price):.2f}") # Output: US total: $107.00
print(f"UK total: £{uk_calculator(item_price):.2f}") # Output: UK total: £120.00
print(f"Japan total: ¥{japan_calculator(item_price):.2f}") # Output: Japan total: ¥110.0023.4.4) 何时使用闭包
当你需要以下能力时,闭包尤其有用:
- 创建具有预配置行为的函数
- 在不使用类的情况下,在函数调用之间保持状态
- 实现需要记住上下文的回调函数(callback functions)
- 创建生成专用函数的函数工厂(function factories)
23.5) 使用 lambda 编写简短的匿名函数
23.5.1) 什么是 Lambda 表达式?
lambda 表达式(lambda expression) 会创建一个小型的匿名函数——一个没有名字的函数。当你在短时间内需要一个简单函数,并且不想用 def 正式定义它时,lambda 表达式很有用。
语法是:
lambda parameters: expressionlambda 接受参数(像普通函数一样),并返回对表达式求值后的结果。下面是一个简单示例:
# 常规函数
def add(x, y):
return x + y
# 等价的 lambda 表达式
add_lambda = lambda x, y: x + y
# 两者工作方式相同
print(add(3, 5)) # Output: 8
print(add_lambda(3, 5)) # Output: 8lambda 表达式只能包含一个表达式——它们不能包含诸如 if、for 或多行代码这样的语句。这一限制让它们保持简单且聚焦。
23.5.2) 将 Lambda 表达式作为参数
当你需要把一个简单函数作为参数传递,又不想定义一个单独的具名函数时,lambda 表达式非常出彩:
# 使用 lambda 按成绩对学生排序
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 92},
{"name": "Charlie", "grade": 78},
{"name": "Diana", "grade": 95}
]
# Instead of defining a separate function:
# def get_grade(student):
# return student["grade"]
# sorted_students = sorted(students, key=get_grade)
# We can use a lambda directly:
sorted_students = sorted(students, key=lambda student: student["grade"])
print("Students sorted by grade:")
for student in sorted_students:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95当函数很简单且只使用一次时,这样会更简洁。lambda lambda student: student["grade"] 等价于一个接收学生并返回其成绩的函数。
23.5.3) 带多个参数的 Lambda
lambda 表达式可以像普通函数一样接受多个参数:
# 使用 lambda 实现计算器操作
operations = {
'add': lambda x, y: x + y,
'subtract': lambda x, y: x - y,
'multiply': lambda x, y: x * y,
'divide': lambda x, y: x / y if y != 0 else "Error"
}
# 使用这些 lambda 表达式
print(operations['add'](10, 5)) # Output: 15
print(operations['multiply'](10, 5)) # Output: 50
print(operations['divide'](10, 0)) # Output: Error注意,我们可以在 lambda 中使用条件表达式(x / y if y != 0 else "Error"),但不能使用 if 语句(它需要多行代码)。
23.5.4) 何时使用 Lambda vs 具名函数
在以下情况下使用 lambda 表达式:
- 函数非常简单(一个表达式)
- 函数只使用一次,或只在非常局部的上下文中使用
- 定义具名函数会带来不必要的冗长
在以下情况下使用具名函数:
- 函数复杂或需要多个语句
- 函数会在多个地方复用
- 函数需要一个描述性名称来增强清晰度
- 函数需要 docstring
23.5.5) Lambda 的限制与替代方案
lambda 表达式有一些重要限制:
# ❌ 这样不行 - lambda 不能包含语句
# bad_lambda = lambda x:
# if x > 0:
# return x
# else:
# return -x
# ✅ 改用条件表达式
absolute_value = lambda x: x if x > 0 else -x
print(absolute_value(-5)) # Output: 5
print(absolute_value(3)) # Output: 3
# ✅ 对于多个操作,使用常规函数
def process_and_double(x):
print(f"Processing: {x}")
return x * 2
result = process_and_double(5) # Output: Processing: 5
print(result) # Output: 10lambda 表达式是针对特定场景的工具。当它们能让代码更清晰、更简洁时,就使用它们。当它们让代码更难理解时,就改用常规的具名函数。
23.6) 使用 map() 和 filter() 搭配简单函数
23.6.1) map() 函数
map() 函数会将给定的 function 应用到可迭代对象(iterable)(例如列表、元组或字符串)的每个元素上,并返回一个包含结果的迭代器(iterator)。它是一种在不编写显式循环的情况下,将集合中的每个元素进行转换的方法。
map(function, iterable, *iterables)参数:
function(必需):一个接受一个或多个参数、对其进行处理并返回值的函数。该函数会对iterable(s)中的每个元素调用一次。iterable(必需):一个序列(列表、元组、字符串等),其元素会被传入function。*iterables(可选):用于多参数function的额外可迭代对象。
如果提供了多个 iterable,function 必须接受相同数量的参数
map() 会在最短的 iterable 被耗尽时停止
返回值:
一个 map 对象(迭代器),其中包含 function 对每个输入元素返回的结果。
重要:map 对象是迭代器,而不是像 list 那样的序列。
# 将列表中的每个数字翻倍
numbers = [1, 2, 3, 4, 5]
def double(x):
return x * 2
# 对每个数字应用 double
doubled = map(double, numbers)
result = list(doubled) # 将 map 对象(迭代器)转换为列表
print(result) # Output: [2, 4, 6, 8, 10]23.6.2) 在 map() 中使用 Lambda
lambda 表达式与 map() 搭配非常合适,适用于简单的转换操作:
# 将温度从摄氏度转换为华氏度
celsius_temps = [0, 10, 20, 30, 40]
fahrenheit_temps = list(map(lambda c: (c * 9/5) + 32, celsius_temps))
print(fahrenheit_temps) # Output: [32.0, 50.0, 68.0, 86.0, 104.0]23.6.3) filter() 函数
filter() 函数会将给定的 function 应用到 iterable 的每个元素上,并返回一个只包含那些使函数返回 True 的元素的迭代器。它是一种在不编写显式循环的情况下,从集合中选择元素的方法。
filter(function, iterable)参数:
function:一个接受一个参数、对其进行判断并返回True或False的函数。该函数会对iterable中的每个元素调用一次。iterable:一个序列(列表、元组、字符串等),其元素会被function测试。
返回值:
一个 filter 对象(迭代器),只包含那些 function 返回 True 的元素。
重要: filter 对象是迭代器,而不是像 list 那样的序列。
示例:
# 只保留偶数
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def is_even(x):
return x % 2 == 0
# 对每个数字应用 is_even,只保留返回 True 的数字
even_numbers = filter(is_even, numbers)
result = list(even_numbers) # 将 filter 对象转换为列表
print(result) # Output: [2, 4, 6, 8, 10]23.6.4) 在 filter() 中使用 Lambda
lambda 表达式常与 filter() 一起使用,以实现更简洁的过滤:
# 筛选通过的学生(grade >= 60)
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 55},
{"name": "Charlie", "grade": 92},
{"name": "Diana", "grade": 48},
{"name": "Eve", "grade": 73}
]
passed = list(filter(lambda s: s["grade"] >= 60, students))
print("Students who passed:")
for student in passed:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Charlie: 92
# Eve: 7323.6.5) 组合使用 map() 和 filter()
你可以把 map() 与 filter() 串联起来,以完成更复杂的转换操作:
# 获取偶数的平方
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 先过滤出偶数,然后对其求平方
even_numbers = filter(lambda x: x % 2 == 0, numbers)
squared = map(lambda x: x ** 2, even_numbers)
result = list(squared)
print(result) # Output: [4, 16, 36, 64, 100]可视化对比:map() vs filter()
关键差异:
map():应用函数来转换每一个元素 → 输出长度相同filter():测试每个元素,只保留通过测试的元素 → 输出长度相等或更短
在本章中,我们探索了 Python 强大的函数式编程(functional programming)特性。我们了解到函数是一等对象,可以像其他值一样被传递与操作,从而实现灵活且可复用的代码模式。我们发现函数可以返回其他函数,从而创建能记住其环境的闭包。我们探索了 lambda 表达式来简洁地定义函数,并使用 map() 与 filter() 优雅地处理集合。
这些概念构成了高级 Python 编程技术的基础。在第 38 章中,我们将基于这些知识来掌握装饰器(decorators),这是 Python 最优雅的特性之一。