《Python编程入门》4.3 一等函数、lambda 与 functools

本节核心事实是:Python 里函数是对象,能被赋值、放进容器、当参数传、当返回值,这正是 lambda 与 functools 存在的原因。接着讲清 lambda 的边界与 PEP 8 建议,比较 map/filter 与推导式的取舍,介绍 itemgetter/attrgetter 替代 lambda,最后演示 reduce、singledispatch 与 functools.cache。

本节目标:理解「函数是一等对象」的含义,掌握 lambda 的使用边界与 PEP 8 建议,会用 sorted(key=...)、operator 与 functools(reduce、singledispatch、cache)写出更简洁的高阶代码。
适用版本:Python 3.12+(实测 3.14.6)

4.3 一等函数、lambda 与 functools

上一节我们看到 partial 能接收一个函数并返回一个新函数,lru_cache 能装饰一个函数。这些用法之所以成立,是因为 Python 里函数不是特殊语法,而是普通对象——它和整数、字符串一样,可以被赋值、被传递、被存储。本节把这个事实讲透。

4.3.1 函数是对象

「一等对象(first-class object)」意味着函数支持对象该有的一切操作。赋值与放进容器只是开始:

def greet(name):
    return f"Hello, {name}"

alias = greet
print(alias("Ada"))

ops = {"inc": lambda x: x + 1, "dec": lambda x: x - 1}
print(ops["inc"](10), ops["dec"](10))

实测输出:

Hello, Ada
11 9

alias = greet 没有调用函数,只是让两个名字指向同一个函数对象;字典里存函数,就能实现「按名字分派」。函数还能当参数传、当返回值:

def apply_twice(fn, value):
    return fn(fn(value))

print(apply_twice(lambda x: x * 2, 3))

def make_multiplier(n):
    return lambda x: x * n

triple = make_multiplier(3)
print(triple(7))

实测输出 12 和 21。apply_twice 是高阶函数——接收函数作参数;make_multiplier 返回函数,它其实是 4.2.8 闭包的另一种写法。一句话:凡是能把函数传来传去的语言机制,背后都是「函数是对象」这一条。

4.3.2 lambda 与 PEP 8 的建议

lambda 是匿名函数的表达式形式,语法只有一种:lambda 参数列表: 单个表达式。它和 def 的关键差异是——函数体只能是一个表达式,不能有语句:

能力deflambda
多条语句 / if / for 语句支持不支持
赋值给变量(有名字)支持需要显式赋值
匿名内联使用否是
有 docstring是否
可调试(栈里显示名字)是(函数名)显示 <lambda>

因为体只能是表达式,lambda 天然适合「只做一件小事、用完即弃」的场景。PEP 8 对此的原文建议是:别把 lambda 赋值给一个名字,那种情况应直接写 def:

# 不推荐:给 lambda 起名,等于用 def 却丢了 def 的好处
f = lambda x: x + 1

# 推荐
def f(x):
    return x + 1

真正该用 lambda 的地方是把函数作为实参内联传入,尤其是排序键。判断标准很朴素:这行 lambda 一眼能读懂就用,读不懂就抽成 def。

4.3.3 sorted 与 key 参数

排序是最常见的高阶函数用法。sorted 的 key 接收一个函数,用来从每个元素里取出比较依据:

people = [
    {"name": "Ada", "age": 36},
    {"name": "Alan", "age": 41},
    {"name": "Grace", "age": 30},
]
print(sorted(people, key=lambda p: p["age"]))
print(sorted(people, key=lambda p: p["name"], reverse=True))

实测输出:

[{'name': 'Grace', 'age': 30}, {'name': 'Ada', 'age': 36}, {'name': 'Alan', 'age': 41}]
[{'name': 'Grace', 'age': 30}, {'name': 'Alan', 'age': 41}, {'name': 'Ada', 'age': 36}]

key 函数对每个元素只调用一次,比较的是它的返回值。这比老式的 cmp 函数高效得多,也让多字段排序变得直观——返回一个元组即可(元组按位置依次比较)。sorted 返回新列表,原列表不变;就地排序用 list.sort()。

4.3.4 map / filter 与推导式的取舍

map 和 filter 是函数式的经典工具,它们也接收函数:

nums = [1, 2, 3, 4, 5, 6]
print("map:", list(map(lambda x: x * x, nums)))
print("filter:", list(filter(lambda x: x % 2 == 0, nums)))
print("推导式:", [x * x for x in nums])
print("过滤推导式:", [x for x in nums if x % 2 == 0])

实测输出:

map: [1, 4, 9, 16, 25, 36]
filter: [2, 4, 6]
推导式: [1, 4, 9, 16, 25, 36]
过滤推导式: [2, 4, 6]

结果完全一致。那该用哪个?Python 社区的普遍偏好是推导式优先,原因有三:其一,map/filter 返回迭代器,想拿到列表还得再套 list();其二,推导式能把「映射 + 过滤」写在一处,map/filter 组合时容易嵌套;其三,推导式对中文读者更接近自然语言。

map/filter 仍有适用场景:已有现成函数(如 map(str.strip, lines))、或只需惰性迭代器时,它们更简洁。判断标准还是可读性,而不是「哪个更函数式」。

4.3.5 operator.itemgetter 与 attrgetter

上面那些 lambda p: p["age"] 其实可以用标准库 operator 模块里用 C 实现的取值器替代,速度更快、也更易读:

from operator import itemgetter, attrgetter

people = [{"name": "Ada", "age": 36}, {"name": "Alan", "age": 41}, {"name": "Grace", "age": 30}]
print(sorted(people, key=itemgetter("age")))

rows = [("a", 3), ("b", 1), ("c", 2)]
print(sorted(rows, key=itemgetter(1)))

实测输出:

[{'name': 'Grace', 'age': 30}, {'name': 'Ada', 'age': 36}, {'name': 'Alan', 'age': 41}]
[('b', 1), ('c', 2), ('a', 3)]

itemgetter(key) 等价于 lambda x: x[key],attrgetter(name) 等价于 lambda x: x.name,都支持多个字段(itemgetter("age", "name") 返回元组):

class P:
    def __init__(self, n, a): self.name, self.age = n, a
    def __repr__(self): return f"P({self.name},{self.age})"

objs = [P("Ada", 36), P("Alan", 41), P("Grace", 30)]
print(sorted(objs, key=attrgetter("age")))

实测输出 [P(Grace,30), P(Ada,36), P(Alan,41)]。凡是能用 operator 表达的函数,就优先用它而不是 lambda——更短、更快、且明确传达了「只是取个字段」的意图。

4.3.6 functools.reduce

reduce 把一个二元函数连续作用到序列上,逐步归约成单个值:

from functools import reduce
import operator

print("reduce 求和:", reduce(lambda acc, x: acc + x, [1, 2, 3, 4, 5]))
print("reduce 带初值:", reduce(lambda acc, x: acc + x, [1, 2, 3], 100))
print("reduce operator.add:", reduce(operator.add, [1, 2, 3, 4]))

实测输出:

reduce 求和: 15
reduce 带初值: 106
reduce operator.add: 10

规则是:取头两个元素算一次,结果再与第三个算,依此类推;传了初值就从初值开始。reduce 虽然强大,但可读性差——一旦归约逻辑稍微复杂,读者就得在脑内展开循环。因此经验法则是:能用 sum、min、max、any、all、math.prod 就优先用它们,只有在没有对应内置函数时才用 reduce。

4.3.7 functools.singledispatch 做类型分派

当你需要「同一个函数名,按第一个参数的类型走不同实现」时,别写一长串 isinstance 判断,用 singledispatch:

from functools import singledispatch

@singledispatch
def render(value):
    return f"未知类型: {value!r}"

@render.register
def _(value: int):
    return f"整数: {value}"

@render.register
def _(value: list):
    return "列表: " + ", ".join(map(str, value))

print(render(42))
print(render([1, 2, 3]))
print(render("text"))

实测输出:

整数: 42
列表: 1, 2, 3
未知类型: 'text'

@singledispatch 装饰的原始函数是默认实现(兜底),后续用 @render.register 按注解类型注册新分支。调用时解释器根据第一个实参的类型选实现,找不到就回退到默认。相比 isinstance 链,它把「分派表」交给装饰器维护,新增类型只需加一个函数。这里「注册函数」被作为实参传给 register——又是「函数是对象」的应用。

4.3.8 functools.cache

Python 3.9 起,functools.cache 提供了最简形式的记忆化,等价于 lru_cache(maxsize=None),但没有容量上限:

from functools import cache

@cache
def slow_square(n):
    print("计算中", n)
    return n * n

print(slow_square(4))
print(slow_square(4))   # 命中缓存,不再打印「计算中」
print(slow_square.cache_info())

实测输出:

计算中 4
16
16
CacheInfo(hits=1, misses=1, maxsize=None, currsize=1)

第二次调用 slow_square(4) 没有再次打印,说明直接返回了缓存结果;cache_info() 显示 1 次命中、1 次未命中。cache 与 lru_cache 的取舍是:确定参数取值有限、不需要淘汰就用 cache;参数空间可能无限膨胀(比如按用户 ID 缓存)就用 lru_cache(maxsize=N) 限制容量。 两者都要求参数可哈希且函数是纯函数。

4.3.9 高阶函数在真实代码里的位置

回头看这一节,你会发现高阶函数不是「炫技」的语法,而是散落在日常代码里的常规工具:

场景用到的机制
按字典字段排序sorted(key=itemgetter(...))
给按钮绑定回调把函数作为参数传给事件注册
装饰器包装函数def wrapper(*args, **kwargs) 接收并返回函数
按类型分派渲染逻辑singledispatch
缓存昂贵计算cache / lru_cache
配置注入partial 预绑定参数

真正要养成的判断是:当你想「对每个元素做点什么」或「把某段行为当数据传来传去」时,先想想有没有现成的高阶函数或标准库工具,而不是条件反射地手写 for 循环或 lambda。

小结

  • 函数是一等对象:可赋值、放进容器、当参数传、当返回值,这是一切高阶用法的基础。
  • lambda 只能写单个表达式;PEP 8 建议不要把 lambda 赋值给名字,它只适合内联传给别的函数。
  • sorted(key=...) 对每个元素只调用一次 key 函数;能用 operator.itemgetter/attrgetter 就优先于 lambda。
  • map/filter 与推导式等价,但推导式通常更可读、更直观;map/filter 在已有现成函数时更简洁。
  • reduce 可读性差,能用 sum/max/math.prod 等内置函数就别用它。
  • singledispatch 用注册表做类型分派,优于手写 isinstance 链;cache(3.9 起)是最简记忆化,参数需可哈希、函数需是纯函数。

到这里,你已经把「数据怎么存(第 3 章)」和「逻辑怎么组织(第 4 章)」两块基础打完了。下一章我们进入模块与包:函数写多了要拆分到多个文件,Python 的 import 到底按什么顺序找文件、sys.path 如何影响结果——这些决定了你写的函数能否被别的文件正确复用。

阅读导航:上一节:4.2 函数:参数传递、默认值与作用域 · 下一节:5.1 import 机制与模块搜索路径 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时