前言
"函数"在 Python 里是个大话题,但真正好用的函数往往很小——只做一件事、 名字说得清、输入输出一看就懂。这一篇就围绕"小函数"这个思路,把函数式风格的 常用工具串一遍:函数当值用、lambda、map/filter/sorted/reduce这些内置工具怎么配合小函数干活。
有个反复出现的误解:以为map、filter返回的是列表。这是 Python 2 时代 的老印象。Python 3 里它们返回的是惰性的迭代器(iterator),需要时才计算 下一个元素,而且只能遍历一次。忘了这一点,就会遇到"打印一次之后再遍历 就空了"的诡异现象。
Python 2 已于 2020 年 1 月 1 日停止维护。本文一律用 Python 3 的写法:print是函数不是语句,map/filter/zip返回迭代器,reduce需要从functools导入——这些和 Python 2 都不一样。
一、函数是可以到处传的值
前面说过函数是对象。既然它是值,就能被放进变量、列表、字典,也能当参数传给 别的函数。所谓"小函数",就是把这些小函数当作积木,交给通用工具去组合。
# 适用于 Python 3.8+
def is_positive(x):
return x > 0
def square(x):
return x * x
nums = [-2, -1, 0, 1, 2]
# 用小函数做"过滤 + 变换"
result = [square(n) for n in nums if is_positive(n)]
print(result) # [1, 4]这两个函数各只有一行,职责单一,名字就是文档。这就是"小函数"的样板。
二、lambda:一次性的小函数
lambda用来写只包含一个表达式的匿名函数,适合当参数就地传。它的能力 是def的子集:不能包含语句、不能写多行逻辑、不能有return。
# 适用于 Python 3.8+
pairs = [("b", 2), ("a", 3), ("c", 1)]
# 按元组第二个元素排序
by_second = sorted(pairs, key=lambda p: p[1])
print(by_second) # [('c', 1), ('b', 2), ('a', 3)]
# 等价于先定义小函数的写法
def second(p):
return p[1]
print(sorted(pairs, key=second)) # 结果一样经验法则:能用def写清楚就别用lambda;只有当函数小到"一行看完、 不值得起名字"时,lambda才划算。
三、map / filter:惰性的转换与筛选
map(func, iterable)把func依次作用到每个元素上,返回一个迭代器;filter(func, iterable)保留让func返回真值的元素,也返回迭代器。 要看结果,用list(...)取出来。
# 适用于 Python 3.8+
nums = [1, 2, 3, 4]
squares = list(map(lambda x: x * x, nums))
print(squares) # [1, 4, 9, 16]
evens = list(filter(lambda x: x % 2 == 0, range(10)))
print(evens) # [0, 2, 4, 6, 8]同样的事用推导式往往更清楚,这也是现在的常见推荐:
# 适用于 Python 3.8+
nums = [1, 2, 3, 4]
print([x * x for x in nums]) # 等价于 map
print([x for x in range(10) if x % 2 == 0]) # 等价于 filtermap和filter的返回值是迭代器,迭代器是"一次性的":遍历过就空了。
# 适用于 Python 3.8+
m = map(str, [1, 2, 3])
print(list(m)) # ['1', '2', '3']
print(list(m)) # [] 已经空了四、sorted 的 key 与 reduce
sorted(iterable, key=..., reverse=...)返回新列表,不改变原对象;key接 受一个函数,用来算排序依据。要"原地排序"用列表自己的list.sort(),它返回None。
# 适用于 Python 3.8+
words = ["banana", "kiwi", "Apple"]
print(sorted(words, key=str.lower)) # ['Apple', 'banana', 'kiwi']
print(sorted(words, key=len)) # ['kiwi', 'Apple', 'banana']functools.reduce(function, iterable, initial)把两个参数的函数累积地作用 到整个序列上,返回一个值;initial可选,作为初始累积值。
# 适用于 Python 3.8+
from functools import reduce
print(reduce(lambda a, b: a + b, [1, 2, 3, 4])) # 10
print(reduce(lambda a, b: a + b, [1, 2, 3, 4], 100)) # 110
print(reduce(lambda a, b: a * b, [1, 2, 3, 4], 1)) # 24注意reduce是functools里的函数,必须导入;它不是内置函数(Python 2 里曾是内置的,3 里移走了)。
五、all / any / min / max
这几个内置函数配合小函数同样好用,且都接受key或生成器表达式:
# 适用于 Python 3.8+
print(all(x > 0 for x in [1, 2, 3])) # True
print(any(x > 2 for x in [1, 2, 3])) # True
people = [("Tom", 30), ("Ann", 25)]
print(max(people, key=lambda p: p[1])) # ('Tom', 30)
print(min(people, key=lambda p: p[1])) # ('Ann', 25)all在遇到第一个假值时就短路返回,any遇到第一个真值就短路,两者都不会遍历完整个序列(如果没必要的话)。
六、把逻辑拆成小函数:一个完整例子
下面用一组小函数处理一份"学生成绩",把过滤、变换、排序、聚合各交给一个职责 单一的函数,最后串起来。这种"每个函数只做一件事、用一个词说清"的写法,是最 容易测试和维护的。
# 适用于 Python 3.8+
students = [
("Tom", 82),
("Ann", 95),
("Bob", 67),
("Amy", 95),
]
def is_pass(student):
"""分数不低于 60 算及格。"""
return student[1] >= 60
def name_of(student):
"""取出姓名。"""
return student[0]
def score_of(student):
"""取出分数。"""
return student[1]
passed = list(filter(is_pass, students))
print([name_of(s) for s in passed]) # ['Tom', 'Ann', 'Bob', 'Amy']
by_score = sorted(students, key=score_of, reverse=True)
print([name_of(s) for s in by_score]) # ['Ann', 'Amy', 'Tom', 'Bob']
total = sum(map(score_of, students))
print(total) # 339拆开之后,每个函数的意图都一目了然;想改"及格线",只要动is_pass一处。 这比只写一个几十行的大函数要可靠得多。
七、工具对照表
| 工具 | 返回 | 惰性 | 备注 |
|---|
map(func, it) | 迭代器 | 是 | 需list()取结果 |
filter(func, it) | 迭代器 | 是 | 需list()取结果 |
sorted(it, key=...) | 新列表 | 否 | 不改原对象 |
list.sort() | None | — | 原地排序 |
reduce(f, it[, init]) | 单个值 | 否 | 需从functools导入 |
all/any | 布尔 | 短路 | 不遍历完 |
常见坑点
- 以为 map / filter 返回列表
``python # ❌ 直接取值会得到迭代器对象 m = map(str, [1, 2]) print(m) # <map object ...> # ✅ 需要列表就转一下 print(list(map(str, [1, 2])))``
- 迭代器被消费两次
``python m = map(str, [1, 2, 3]) # ❌ 第一次遍历后迭代器已耗尽 for x in m: pass print(list(m)) # [] # ✅ 先物化成列表再复用 items = list(m)``
- 把 sort 的返回值当排序结果
``python data = [3, 1, 2] # ❌ sort 原地排序并返回 None result = data.sort() # ✅ 要么直接用 data,要么用 sorted result = sorted(data)``
- lambda 里塞语句
``python # ❌ lambda 只能是一个表达式,不能有语句 f = lambda x: if x > 0: return x # ✅ 用 def def f(x): if x > 0: return x``
- 循环里用 lambda 全取到最后一个值
``python # ❌ 延迟绑定,调用时 i 已是最后一个 funcs = [lambda: i for i in range(3)] print([f() for f in funcs]) # [2, 2, 2] # ✅ 用默认参数固定当前值 funcs = [lambda i=i: i for i in range(3)] print([f() for f in funcs]) # [0, 1, 2]``
- 忘了导入 reduce
``python # ❌ NameError:3 里 reduce 不再是内置的 reduce(lambda a, b: a + b, [1, 2]) # ✅ 从 functools 导入 from functools import reduce``
- 对空序列用 reduce 且没给初始值
``python from functools import reduce # ❌ 空序列 + 无初始值 → TypeError reduce(lambda a, b: a + b, []) # ✅ 给一个初始值 reduce(lambda a, b: a + b, [], 0)``
总结
| 想做的事 | 推荐写法 |
|---|
| 一次性小函数 | lambda,且只写表达式 |
| 命名清晰的小函数 | def,一行看完 |
| 逐元素变换 / 筛选 | 推导式优先,map/filter次之 |
| 排序依据 | sorted(key=...),注意返回新列表 |
| 累积归约 | functools.reduce,记得导入 |
| 全真 / 存在 | all/any,且会短路 |
"大话题"之所以能落到"小函数",关键是两件事:每个函数只做一件事,以及把函数当成值交给通用工具去组合。记住map/filter返回的是只消费一次 的迭代器,就能避开最常见的那几个坑。