前言
高阶函数(higher-order function)指接收函数作为参数、或把函数作为返回值的函数。Python 里这类函数非常多,最常被点名的有map、filter、functools.reduce、sorted、max/min以及functools.partial。它们共同的价值是:把「做什么」抽出来当参数传,让调用方自己决定处理逻辑。
第一个常见误解是把高阶函数当成「更快的写法」。多数情况下,同样的事情用列表推导式(list comprehension)写会更短也更好读,map/filter并不存在通用的性能优势。选用的标准应该是可读性:已经有现成函数可以传,就用map/filter;逻辑需要现写,列表推导式通常更清楚。
第二个必须纠正的地方是 Python 2 与 Python 3 的断崖。Python 2.7 已于 2020 年 1 月 1 日停止维护。在 Python 2 里map和filter返回列表、reduce是内置函数;在 Python 3 里map/filter返回迭代器,而reduce已经从内置命名空间移除,必须从functools导入。旧代码直接搬过来,轻则拿到迭代器对象,重则NameError: name 'reduce' is not defined。
一、map与filter
# 适用于 Python 3.0+
nums = [1, 2, 3, 4, 5]
squares = map(lambda x: x * x, nums)
print(squares) # <map object at 0x...>:是迭代器,不是列表
print(list(squares)) # [1, 4, 9, 16, 25]
evens = filter(lambda x: x % 2 == 0, nums)
print(list(evens)) # [2, 4]要点:
- 两者都返回迭代器,只能遍历一次;要落成列表得显式
list(...)。 filter的第一个参数可以传None,此时按真值过滤:filter(None, [0, 1, "", "a"])得到[1, 'a']。map支持多个可迭代对象,长度以最短的为准:map(lambda a, b: a + b, [1, 2], [10, 20, 30])得到[11, 22]。
同一件事用推导式的写法:
# 适用于 Python 3.0+
nums = [1, 2, 3, 4, 5]
squares = [x * x for x in nums] # 替代 map
evens = [x for x in nums if x % 2 == 0] # 替代 filter一旦出现嵌套或条件分支,推导式通常明显更好读。反而当处理逻辑已经是现成的具名函数时,map更短:
# 适用于 Python 3.0+
words = [" hi ", "world "]
print(list(map(str.strip, words))) # ['hi', 'world']二、functools.reduce
# 适用于 Python 3.0+
from functools import reduce
total = reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
print(total) # 15
with_init = reduce(lambda x, y: x + y, [], 0)
print(with_init) # 0签名是functools.reduce(function, iterable, /[, initial]),文档给出的近似实现是:把iterable的第一个元素当作初始累积值(若提供了initial则用它),然后从左到右依次执行value = function(value, element)。
三个必须知道的行为:
iterable为空且没有initial时抛TypeError。iterable只有一个元素且没有initial时,直接返回那个元素,function一次都不调用。- Python 3.14 起
initial可以作为关键字参数传入(reduce(f, it, initial=0));在此之前的版本里它只能按位置传。
大多数「求和 / 求积 / 求最值」的场景有更专用的内置函数,不必上reduce:
| 需求 | 推荐写法 | 说明 |
|---|
| 求和 | sum(iterable, start=0) | start可位置可关键字 |
| 求最值 | max(iterable, *, key=None, default=...) | 空序列需给default |
| 求积 | math.prod(iterable, *, start=1) | 需要 Python 3.8+ |
| 字符串拼接 | "".join(parts) | 避免在reduce里反复拼接 |
| 累积中间结果 | itertools.accumulate(iterable, func=operator.add) | 返回迭代器,产出每一步 |
| 逻辑与 / 或 | all(iterable)/any(iterable) | 短路求值 |
三、sorted/max/min的key
这三个函数都通过key参数接收一个函数,用它把每个元素映射成可比较的键。
# 适用于 Python 3.0+
rows = [("bob", 30), ("alice", 25), ("carol", 35)]
print(sorted(rows, key=lambda r: r[1])) # 按年龄升序
print(sorted(rows, key=lambda r: r[1], reverse=True))# 按年龄降序
print(max(rows, key=lambda r: r[1])) # ('carol', 35)
print(min(rows, key=lambda r: r[1])) # ('alice', 25)签名务必记准:sorted(iterable, *, key=None, reverse=False)、min(iterable, *, key=None, default=...)——key是仅关键字参数,写成sorted(rows, lambda r: r[1])会把 lambda 当成第二个可迭代对象,直接报错。
key只接受一个参数,但可以返回元组实现多级排序:
# 适用于 Python 3.0+
data = [("bob", 90), ("alice", 90), ("carol", 85)]
print(sorted(data, key=lambda r: (-r[1], r[0])))
# [('alice', 90), ('bob', 90), ('carol', 85)]:先按分数降序,再按名字升序对取下标、取属性、调方法这三类常见需求,operator模块比 lambda 更直接:
# 适用于 Python 3.0+
from operator import itemgetter, attrgetter, methodcaller
rows = [("bob", 30), ("alice", 25)]
print(sorted(rows, key=itemgetter(1))[0]) # ('alice', 25)
print(itemgetter(0)(("x", "y"))) # x
print(methodcaller("upper")("abc")) # ABCitemgetter(1)等价于lambda r: r[1],但它是 C 实现且不用建闭包。
实战:一个可组合的数据处理链
# 适用于 Python 3.0+
from functools import reduce
from operator import itemgetter
orders = [
{"id": 1, "user": "alice", "amount": 120},
{"id": 2, "user": "bob", "amount": 80},
{"id": 3, "user": "alice", "amount": 200},
{"id": 4, "user": "carol", "amount": 50},
]
# 1) 筛出大额订单
big = list(filter(lambda o: o["amount"] >= 100, orders))
print([o["id"] for o in big]) # [1, 3]
# 2) 按金额降序
big.sort(key=itemgetter("amount"), reverse=True)
print([o["id"] for o in big]) # [3, 1]
# 3) 汇总金额
total = reduce(lambda acc, o: acc + o["amount"], big, 0)
print(total) # 320
# 4) 用 sum + 生成器表达更直白
print(sum(o["amount"] for o in big)) # 320
# 5) 按用户分组统计(不用排序,用字典累加)
from collections import defaultdict
by_user = defaultdict(int)
for o in orders:
by_user[o["user"]] += o["amount"]
print(dict(sorted(by_user.items()))) # {'alice': 320, 'bob': 80, 'carol': 50}第 5 步顺带说明:itertools.groupby(iterable, key=None)也能分组,但它只合并相邻的相同键,用之前必须先按同一个键排序,否则同一个键会被拆成多组。这是分组类需求里最常见的一个坑,新手往往误以为groupby会自动去重归并。
一段zip+map的组合示例,注意strict参数需要 3.10+:
# 适用于 Python 3.10+
names = ["a", "b", "c"]
scores = [1, 2, 3]
pairs = zip(names, scores, strict=True) # 长度不一致会抛 ValueError
print(list(pairs)) # [('a', 1), ('b', 2), ('c', 3)]
print(list(map(lambda p: p[0] * p[1], zip(names, scores))))
# ['a', 'bb', 'ccc']常见坑点
1. 沿用 Python 2 的reduce内置写法
❌ 错误写法:
print(reduce(lambda x, y: x + y, [1, 2, 3])) # NameError: name 'reduce' is not defined✅ 正确写法:
from functools import reduce
print(reduce(lambda x, y: x + y, [1, 2, 3])) # 6Python 3.0 起reduce已从内置命名空间移除(官方whatsnew/3.0明确写了「Removedreduce(). Usefunctools.reduce()」)。
2. 以为map/filter返回列表
❌ 错误写法:
result = map(str.upper, ["a", "b"])
print(result[0]) # TypeError: 'map' object is not subscriptable✅ 正确写法:
result = list(map(str.upper, ["a", "b"]))
print(result[0]) # A3. 迭代器只能用一次,第二次遍历为空
❌ 错误写法:
m = map(lambda x: x * 2, [1, 2, 3])
print(list(m)) # [2, 4, 6]
print(list(m)) # [] —— 已经被耗尽✅ 正确写法:先落成列表再复用,或者每次重新生成:
data = [1, 2, 3]
print([x * 2 for x in data]) # [2, 4, 6]
print([x * 2 for x in data]) # [2, 4, 6]4. 把 lambda 当成sorted的第二个位置参数
❌ 错误写法:
sorted(rows, lambda r: r[1]) # TypeError: sorted expected 1 argument, got 2✅ 正确写法:key是仅关键字参数:
sorted(rows, key=lambda r: r[1])5.reduce在空序列上没给initial
❌ 错误写法:
reduce(lambda x, y: x + y, []) # TypeError✅ 正确写法:
reduce(lambda x, y: x + y, [], 0) # 06.max/min在空序列上报错
❌ 错误写法:
max([], key=len) # ValueError: max() iterable argument is empty✅ 正确写法:用default参数(max/min支持,需要 Python 3.4+):
print(max([], key=len, default=None)) # None7. 循环里生成 lambda 传进高温函数
❌ 错误写法:
conds = [lambda x: x > i for i in range(3)]
print([c(1) for c in conds]) # [False, False, False] —— 全都用了 i=2
print([c(2) for c in conds]) # [False, False, False]✅ 正确写法:
conds = [lambda x, i=i: x > i for i in range(3)]
print([c(1) for c in conds]) # [True, False, False]
print([c(2) for c in conds]) # [True, True, False]对照c(1)这一行就能看出问题:错误写法里所有闭包都读到最终的i=2,因此1 > 2一律为False;正确写法则让三个函数分别绑定 0、1、2。
8. 误以为groupby会自动归并相同键
❌ 错误写法:
from itertools import groupby
data = [("a", 1), ("b", 2), ("a", 3)]
print([(k, len(list(g))) for k, g in groupby(data, key=lambda t: t[0])])
# [('a', 1), ('b', 1), ('a', 1)] —— 'a' 被拆成了两组✅ 正确写法:先按同一个键排序:
from itertools import groupby
data = sorted([("a", 1), ("b", 2), ("a", 3)], key=lambda t: t[0])
print([(k, len(list(g))) for k, g in groupby(data, key=lambda t: t[0])])
# [('a', 2), ('b', 1)]总结
| 函数 | 返回 | 关键参数 | 版本注意 |
|---|
map(func, *iterables) | 迭代器 | 无 | Python 3 返回迭代器,2 里返回列表 |
filter(func, iterable) | 迭代器 | func可为None | 同上 |
functools.reduce(func, iterable, /[, initial]) | 单值 | initial | Python 3 移出内置;3.14 起initial可作为关键字 |
sorted(iterable, *, key, reverse) | 新列表 | key、reverse仅关键字 | — |
max/min(..., *, key, default) | 单个元素 | default | default需 3.4+ |
zip(*iterables, strict=False) | 迭代器 | strict | strict需 3.10+ |
math.prod(iterable, *, start=1) | 数值 | start | 需 3.8+ |
高阶函数的选型原则是「可读性优先」:逻辑现成、只有一步变换,用map/filter/operator工厂;逻辑要现写、带条件或嵌套,用列表推导式;求和的场景优先sum、any、all这类专用内置,reduce留给真正需要自定义累积逻辑的场合。另外务必记住三条语义:key是仅关键字参数、迭代器只能消费一次、groupby只合并相邻键。