Python 高阶函数:把函数当"积木"来搭建程序
引言:乐高积木的启示
玩乐高时,你不需要自己造每块积木——只需要把现有的积木块组合起来,就能搭出城堡、飞船、机器人。
Python 的高阶函数(Higher-order Function)就是这个思路:把函数本身当作积木块,传给另一个函数,或者从另一个函数中返回。这种"函数套函数"的玩法,让代码的抽象能力和复用性达到全新高度。
这篇我们系统讲解四大核心高阶函数:map()、reduce()、filter()、sorted(),每个都从定义、语法、实战、性能、误区五个维度彻底吃透。
一、什么是高阶函数?
1.1 函数也是"值"
先看一个看似奇怪的现象:
>>> abs(-10)
10
>>> abs # 不加括号,是什么?
<built-in function abs>
>>> f = abs # 把函数赋值给变量
>>> f(-10)
10 # 变量 f 现在就是 abs 函数本身核心结论:在 Python 中,函数名就是变量,它指向一个函数对象。函数可以像数字、字符串一样被赋值、被传递。
生活化理解:函数就像一把钥匙——你可以把钥匙挂在墙上(赋值给变量),也可以把钥匙交给别人(作为参数传递),别人拿着这把钥匙就能开门(调用函数)。
1.2 高阶函数的定义
一个函数如果可以接收另一个函数作为参数,或者返回一个函数,它就是高阶函数。
def add(x, y, f):
return f(x) + f(y)
print(add(-5, 6, abs)) # f 接收 abs 函数,结果:abs(-5) + abs(6) = 11生活化理解:就像你去餐厅,不是点"红烧肉",而是说"给我做这道菜,用这种做法"——"这道菜"是数据,"这种做法"是函数,餐厅就是高阶函数。
二、map():批量加工的流水线
2.1 官方定义与设计初衷
map() 接收一个函数和一个可迭代对象,把函数依次作用到每个元素上,返回一个迭代器。
设计初衷来自函数式编程的"映射"思想:把一种数据批量转换成另一种数据,而不改变原数据。
2.2 语法格式
map(function, iterable, ...)| 参数 | 说明 |
|---|---|
function | 要应用的函数,接收一个元素,返回一个结果 |
iterable | 可迭代对象(list、tuple、str 等) |
... | 可传多个 iterable,function 需对应多个参数 |
返回值:Python 3 中返回迭代器(Iterator),惰性计算。
2.3 基础用法
# 场景 1:数字平方
def square(x):
return x * x
result = map(square, [1, 2, 3, 4, 5])
print(list(result)) # [1, 4, 9, 16, 25]
# 场景 2:数字转字符串
result = map(str, [1, 2, 3, 4, 5])
print(list(result)) # ['1', '2', '3', '4', '5']
# 场景 3:多个序列同时处理(Python 3 特性)
result = map(lambda x, y: x + y, [1, 2, 3], [10, 20, 30])
print(list(result)) # [11, 22, 33]生活化理解:map() 就像工厂流水线——原料(数据)从一头进去,机器(函数)统一加工,成品从另一头出来。
2.4 进阶实战
案例:批量处理用户输入
# 用户输入的名字大小写混乱,统一规范为首字母大写
def normalize(name):
return name.capitalize() # 首字母大写,其余小写
L1 = ['adam', 'LISA', 'barT', 'ALICE']
L2 = list(map(normalize, L1))
print(L2) # ['Adam', 'Lisa', 'Bart', 'Alice']案例:批量提取文件扩展名
import os
files = ['a.txt', 'b.py', 'c.jpg', 'd.pdf']
exts = list(map(lambda f: os.path.splitext(f)[1], files))
print(exts) # ['.txt', '.py', '.jpg', '.pdf']2.5 与列表推导式的对比
# map 写法
list(map(lambda x: x * x, range(10)))
# 列表推导式写法
[x * x for x in range(10)]| 对比维度 | map() | 列表推导式 |
|---|---|---|
| 可读性 | 函数名清晰时更简洁 | 逻辑复杂时更直观 |
| 性能 | 略快(C 实现) | 略慢(Python 解释) |
| 惰性 | 返回迭代器,省内存 | 返回列表,占内存 |
| 多序列 | 原生支持 map(f, seq1, seq2) | 需用 zip 配合 |
| 适用场景 | 简单转换、大数据流 | 复杂逻辑、需要索引时 |
经验法则:简单转换用 map(),复杂逻辑用列表推导式;处理超大数据流时优先 map()(惰性)。
2.6 常见误区
误区 1:忘记 map 返回迭代器,直接当列表用
result = map(str, [1, 2, 3])
print(result) # <map object at 0x...>,不是列表!
print(result[0]) # TypeError: 'map' object is not subscriptable纠正:需要列表时显式转换 list(result),或直接用 for 循环迭代。
误区 2:在迭代中修改原列表
L = [1, 2, 3]
result = map(lambda x: x * 2, L)
L.append(4) # 危险!map 是惰性的,还没开始算
print(list(result)) # [2, 4, 6, 8] —— 4 也被算进去了纠正:map 创建后不要修改原序列,或先转成列表再修改。
误区 3:多序列时长度不一致
list(map(lambda x, y: x + y, [1, 2], [10, 20, 30])) # [11, 22]
# 多余的 30 被静默丢弃,不会报错!纠正:长度不一致时,map 会在最短序列处停止。如需严格匹配,先用 zip 或检查长度。
三、reduce():累积汇总的折叠器
3.1 官方定义与设计初衷
reduce() 把一个二元函数(接收两个参数)累积作用到序列上,把序列"折叠"成一个值。
设计初衷来自函数式编程的"折叠/累积"思想:把一串数据通过某种规则合并成一个结果。
注意:Python 3 中
reduce()被移到了functools模块,需要显式导入。
3.2 语法格式
from functools import reduce
reduce(function, iterable[, initial])| 参数 | 说明 |
|---|---|
function | 二元函数,接收两个参数,返回一个值 |
iterable | 可迭代对象 |
initial | 可选,初始值。提供后,先计算 function(initial, first_element) |
返回值:累积后的最终结果(单个值)。
3.3 执行原理
reduce(f, [x1, x2, x3, x4])
# 等价于
f(f(f(x1, x2), x3), x4)生活化理解:就像滚雪球——先捏一个小雪球(x1),然后每遇到新雪(x2, x3...)就裹上一层,雪球越滚越大。
3.4 基础用法
from functools import reduce
# 场景 1:求和
def add(x, y):
return x + y
print(reduce(add, [1, 3, 5, 7, 9])) # 25
# 场景 2:序列转整数
def fn(x, y):
return x * 10 + y
print(reduce(fn, [1, 3, 5, 7, 9])) # 13579
# 场景 3:带初始值
print(reduce(add, [1, 2, 3], 100)) # 106 = 100+1+2+33.5 进阶实战
案例:手写 str2int
from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
'5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def str2int(s):
def char2num(ch):
return DIGITS[ch]
return reduce(lambda x, y: x * 10 + y, map(char2num, s))
print(str2int('13579')) # 13579案例:手写 str2float
from functools import reduce
def str2float(s):
# 拆分整数部分和小数部分
parts = s.split('.')
int_part = reduce(lambda x, y: x * 10 + y, map(int, parts[0]))
float_part = reduce(lambda x, y: x * 10 + y, map(int, parts[1]))
return int_part + float_part / (10 ** len(parts[1]))
print(str2float('123.456')) # 123.456案例:求列表最大值的"reduce 版"
from functools import reduce
def my_max(L):
return reduce(lambda a, b: a if a > b else b, L)
print(my_max([3, 1, 4, 1, 5, 9, 2, 6])) # 93.6 与同类实现的对比
| 场景 | reduce 写法 | 推荐替代方案 | 原因 |
|---|---|---|---|
| 求和 | reduce(add, L) | sum(L) | 内置函数更快更清晰 |
| 求积 | reduce(mul, L) | math.prod(L) | Python 3.8+ 内置 |
| 字符串拼接 | reduce(add, strs) | ''.join(strs) | join 是 C 实现,极快 |
| 找最大值 | reduce(max2, L) | max(L) | 内置函数 |
| 复杂累积 | reduce(f, L) | 视情况 | 无内置时才用 reduce |
性能真相:reduce() 是 Python 解释器逐次调用函数,性能通常低于内置的 sum()、max() 等 C 实现函数。它的价值在于表达力,而非性能。
3.7 常见误区
误区 1:空列表没有初始值
reduce(lambda x, y: x + y, []) # TypeError: reduce() of empty iterable with no initial value纠正:空序列必须提供 initial 参数,或先检查长度。
误区 2:忘记 reduce 需要导入
reduce(add, [1, 2, 3]) # NameError: name 'reduce' is not defined纠正:from functools import reduce。
误区 3:滥用 reduce 导致可读性下降
# 可读性极差的写法
reduce(lambda a, b: a + [b] if b not in a else a, L, [])纠正:复杂逻辑优先用普通循环。Python 之禅:Readability counts(可读性很重要)。
四、filter():条件筛选的过滤网
4.1 官方定义与设计初衷
filter() 接收一个判断函数和一个可迭代对象,把判断函数依次作用于每个元素,返回值为 True 的保留,False 的丢弃,返回迭代器。
设计初衷:从数据中筛出符合条件的子集,是"过滤"思想的函数式表达。
4.2 语法格式
filter(function, iterable)| 参数 | 说明 |
|---|---|
function | 判断函数,返回 True/False(或能转为布尔值的对象) |
iterable | 可迭代对象 |
返回值:迭代器(惰性)。
4.3 基础用法
# 场景 1:只保留奇数
def is_odd(n):
return n % 2 == 1
print(list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))) # [1, 5, 9, 15]
# 场景 2:删除空字符串和空白字符串
def not_empty(s):
return s and s.strip() # None 和 '' 返回 False,' ' 经 strip() 变 '' 也返回 False
print(list(filter(not_empty, ['A', '', 'B', None, 'C', ' '])))
# ['A', 'B', 'C']
# 场景 3:function 传 None,过滤掉所有"假值"
print(list(filter(None, [1, 0, '', 'a', [], [1], None])))
# [1, 'a', [1]]生活化理解:filter() 就像安检门——人(数据)挨个过,安检仪(函数)扫描,合格的放行,不合格的拦下。
4.4 进阶实战:埃氏筛法求素数
这是 filter() 最经典的应用——用无限生成器 + 惰性过滤表示"全体素数":
def _odd_iter():
"""生成从 3 开始的无限奇数序列"""
n = 1
while True:
n = n + 2
yield n
def _not_divisible(n):
"""返回一个判断函数:x 不能被 n 整除"""
return lambda x: x % n > 0
def primes():
"""生成全体素数的无限序列"""
yield 2
it = _odd_iter() # 初始序列:3, 5, 7, 9, 11, ...
while True:
n = next(it) # 取第一个数,必是素数
yield n
it = filter(_not_divisible(n), it) # 筛掉 n 的倍数,生成新序列
# 打印 100 以内的素数
for n in primes():
if n < 100:
print(n)
else:
break精妙之处:filter() 返回的是惰性迭代器,每次 next(it) 时才真正计算。primes() 可以无限生成素数,内存占用却始终是 O(1)。
4.5 与列表推导式的对比
# filter 写法
list(filter(lambda x: x % 2 == 0, range(10)))
# 列表推导式写法
[x for x in range(10) if x % 2 == 0]| 对比维度 | filter() | 列表推导式 |
|---|---|---|
| 可读性 | 条件函数有名字时更清晰 | 条件简单时更直观 |
| 性能 | 略快(C 实现) | 略慢 |
| 惰性 | 返回迭代器 | 返回列表 |
| 复杂条件 | 函数可以复用 | 条件内联,不易复用 |
| 适用场景 | 大数据流、条件需复用 | 小数据、简单条件 |
4.6 常见误区
误区 1:filter 后忘记转列表
result = filter(is_odd, [1, 2, 3])
print(result) # <filter object at 0x...>
print(len(result)) # TypeError: object of type 'filter' has no len()纠正:需要长度或索引时,先 list(result)。
误区 2:在 filter 中修改原序列
L = [1, 2, 3, 4, 5]
f = filter(lambda x: x > 2, L)
L.remove(3)
print(list(f)) # [4, 5] —— 3 被删除了!纠正:filter 是惰性的,创建后修改原序列会影响结果。
误区 3:判断函数有副作用
def bad_filter(x):
print(f"checking {x}") # 副作用!
return x > 0
f = filter(bad_filter, [1, -2, 3])
# 此时什么都没打印,因为 filter 是惰性的纠正:filter 的条件函数应该是纯函数(无副作用),副作用操作请在 for 循环或 map 中做。
五、sorted():自定义规则的排序大师
5.1 官方定义与设计初衷
sorted() 对可迭代对象排序,返回新的列表。通过 key 参数传入函数,可以自定义排序规则。
设计初衷:排序的核心是比较,但不同数据类型的比较规则千差万别。key 函数把"怎么比"抽象出来,让 sorted() 专注于"怎么排"。
5.2 语法格式
sorted(iterable, *, key=None, reverse=False)| 参数 | 说明 |
|---|---|
iterable | 可迭代对象 |
key | 函数,作用于每个元素,返回用于比较的"键" |
reverse | True 为降序,False 为升序(默认) |
返回值:新的排序后列表(不修改原序列)。
5.3 基础用法
# 场景 1:数字排序
print(sorted([36, 5, -12, 9, -21])) # [-21, -12, 5, 9, 36]
# 场景 2:按绝对值排序
print(sorted([36, 5, -12, 9, -21], key=abs)) # [5, 9, -12, -21, 36]
# 场景 3:字符串按长度排序
print(sorted(['apple', 'fig', 'banana'], key=len)) # ['fig', 'apple', 'banana']
# 场景 4:忽略大小写排序
print(sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower))
# ['about', 'bob', 'Credit', 'Zoo']
# 场景 5:反向排序
print(sorted([1, 2, 3], reverse=True)) # [3, 2, 1]key 函数的原理:sorted() 先把 key 作用于每个元素,得到"键"列表,然后按键排序,最后按对应关系返回原元素。
原始: [36, 5, -12, 9, -21]
key=abs 后: [36, 5, 12, 9, 21]
按键排序: [5, 9, 12, 21, 36]
返回原元素: [5, 9, -12, -21, 36]5.4 进阶实战
案例:学生成绩多维度排序
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
# 按名字排序
print(sorted(L, key=lambda t: t[0]))
# [('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)]
# 按成绩从高到低
print(sorted(L, key=lambda t: t[1], reverse=True))
# [('Adam', 92), ('Lisa', 88), ('Bob', 75), ('Bart', 66)]
# 先按成绩,成绩相同再按名字(多级排序)
L2 = [('Adam', 92), ('Lisa', 88), ('Bob', 88), ('Bart', 66)]
print(sorted(L2, key=lambda t: (-t[1], t[0])))
# [('Adam', 92), ('Bob', 88), ('Lisa', 88), ('Bart', 66)]
# 技巧:成绩取负实现降序,名字正序案例:字典列表按字段排序
students = [
{'name': 'Alice', 'score': 85, 'age': 20},
{'name': 'Bob', 'score': 92, 'age': 19},
{'name': 'Charlie', 'score': 78, 'age': 21},
]
# 按分数排序
print(sorted(students, key=lambda s: s['score'], reverse=True))
# 按年龄,再按分数
print(sorted(students, key=lambda s: (s['age'], -s['score'])))案例:自定义类排序
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def __repr__(self):
return f'{self.name}: {self.score}'
students = [Student('Alice', 85), Student('Bob', 92), Student('Charlie', 78)]
print(sorted(students, key=lambda s: s.score, reverse=True))
# [Bob: 92, Alice: 85, Charlie: 78]5.5 与同类实现的对比
| 场景 | sorted() | list.sort() | 适用建议 |
|---|---|---|---|
| 原列表需保留 | 返回新列表,原列表不变 | 原地排序,原列表改变 | 用 sorted() |
| 只需排序结果 | 返回新列表 | 返回 None | 用 sorted() |
| 对任意可迭代对象 | 支持(如 dict、set、str) | 仅 list | 用 sorted() |
| 内存敏感大列表 | 创建新列表,占内存 | 原地排序,省内存 | 用 list.sort() |
性能:sorted() 和 list.sort() 底层都是 Timsort(Python 内置排序算法),时间复杂度 O(n log n),性能差异主要在是否复制列表。
5.6 常见误区
误区 1:以为 sorted() 会修改原列表
L = [3, 1, 2]
sorted(L)
print(L) # [3, 1, 2] —— 原列表没变!纠正:sorted() 返回新列表。如需原地排序,用 L.sort()。
误区 2:key 函数返回不可比较的类型
sorted([1, '2', 3], key=lambda x: x) # TypeError: '<' not supported between 'str' and 'int'纠正:key 函数必须返回可比较的同类型值。混合类型需先统一转换。
误区 3:多级排序时逻辑写反
# 错误:先按名字,再按成绩
sorted(L, key=lambda t: (t[0], t[1]))
# 正确:先按成绩(主要),再按名字(次要)
sorted(L, key=lambda t: (t[1], t[0]))纠正:元组比较从左到右,左边是主要排序键,右边是次要。
误区 4:对字典用 sorted() 时期望排序 value
d = {'a': 3, 'b': 1, 'c': 2}
print(sorted(d)) # ['a', 'b', 'c'] —— 默认按 key 排序!
# 按 value 排序
print(sorted(d, key=lambda k: d[k])) # ['b', 'c', 'a']纠正:sorted(dict) 默认排序的是键(key),不是值。
六、四大函数速查表
| 函数 | 作用 | 语法 | 返回值 | 惰性 | 典型场景 |
|---|---|---|---|---|---|
map() | 批量转换 | map(f, seq) | 迭代器 | 是 | 统一加工数据 |
reduce() | 累积折叠 | reduce(f, seq, init) | 单个值 | 否 | 合并、聚合 |
filter() | 条件筛选 | filter(f, seq) | 迭代器 | 是 | 过滤数据 |
sorted() | 排序 | sorted(seq, key=f, reverse=) | 新列表 | 否 | 自定义排序 |
组合使用示例:找出 1~100 中所有偶数的平方,按从大到小排序,取前 3 个
from functools import reduce
result = sorted(
map(lambda x: x * x, filter(lambda x: x % 2 == 0, range(1, 101))),
reverse=True
)[:3]
print(result) # [10000, 9604, 9216]七、动手练习
- 基础题:用
map()把['hello', 'world']变成['HELLO', 'WORLD']。 - 进阶题:用
reduce()求[3, 5, 7, 9]的乘积。 - 挑战题:用
filter()找出 1~1000 中所有回文数(如 121, 1331)。 - 思考题:
map(f, filter(g, seq))和filter(g, map(f, seq))有什么区别?什么场景下用哪种顺序?
小结
- 高阶函数:能接收函数作为参数,或返回函数的函数。函数在 Python 中是一等公民。
- map():批量转换,返回惰性迭代器。简单转换优先用它,复杂逻辑用列表推导式。
- reduce():累积折叠,返回单个值。优先用
sum()、max()、join()等内置函数,无内置时才用 reduce。 - filter():条件筛选,返回惰性迭代器。条件函数应为纯函数,无副作用。
- sorted():自定义排序,
key函数是灵魂。多级排序用元组,注意主次顺序。 - 核心心法:高阶函数是"组合优于继承"的函数式体现——把简单的小函数组合成复杂功能,代码更模块化、更可复用、更易测试。