引言
这是第四章「函数」的第 4 篇。前三篇分别讲清楚了 def 如何定义命名函数(第 1 篇)、作用域与 LEGB 规则如何决定变量可见性(第 2 篇)、以及 *args / **kwargs 如何处理不确定数量的参数(第 3 篇)。本篇继续往「函数作为一等公民」这条线上推进:Python 允许把函数当作参数传给其他函数、作为返回值返回——这种能力催生了 高阶函数(higher-order function) 的概念,而 lambda 正是书写这类一次性函数的最简洁语法。
你可能会疑惑:既然已经有 def 了,为什么还需要 lambda?答案很简单——当某个函数只被调用一次、逻辑又足够短小时,为它专门写一个命名函数会产生大量「一次性代码」,徒增命名焦虑;lambda 让这类「即写即用、用完即弃」的函数可以内联在调用处,使代码更紧凑。但与此同时,lambda 也有明确的局限:它只能是单表达式,不能包含语句,更不能被赋值为一个「真正的名字」反复引用。理解这些边界,才能知道什么时候该用 lambda、什么时候该老实写 def。
本篇先讲 lambda 的语法与限制,再讲它与 map / filter / sorted / functools.reduce 如何配合工作,最后讨论它和 def 的适用场景区别。读完之后,你不仅能写出正确的 lambda 代码,还能清楚地解释「为什么我不应该在这里用 lambda」。
概念与原理
什么是 lambda
lambda 是 Python 内置的语法关键字,用来创建匿名函数(anonymous function)——即没有名称的函数对象。它的形式是:
lambda <参数列表>: <表达式>
注意,冒号右边必须是单个表达式,而不是语句块。这个表达式会被自动求值并作为函数的返回值,不需要写 return。lambda 本身是一个表达式(expression),可以在任何允许表达式出现的地方使用,比如直接传给另一个函数作为参数。
从实现角度看,lambda 和 def 生成的东西在类型上是完全一样的:
>>> type(lambda x: x + 1)
<class 'function'>
它们都返回一个 function 对象,都支持被调用、被传递、被存储在变量中。唯一的区别在于书写方式和语言约束:def 是语句,必须独占一行(或多行缩进块),并且生成一个绑定到名字的函数;lambda 是表达式,可以嵌入在其他表达式中,生成的函数默认没有名字(__name__ 属性为 '<lambda>')。
lambda 的三条硬约束
1. 只能是单个表达式
# ✅ 合法:单表达式
f = lambda x: x * 2
# ❌ 非法:多行 / 语句
g = lambda x:
if x > 0:
return x
else:
return -x
如果你发现某个函数的逻辑需要 if/else、循环、赋值甚至 print,那就应该老老实实用 def。lambda 的设计意图是「简短的、一次性的映射逻辑」,不是替代整个函数定义。
2. 不能进行赋值操作(walrus operator 例外)
Python 不允许在 lambda 体内写普通的赋值语句:
# ❌ 语法错误
lambda x: (y = x * 2, y + 1)
# ✅ 用 walrus operator(:=)可以间接赋值,但不推荐
lambda x: ((y := x * 2), y + 1)[1]
walrus operator 是 Python 3.8 引入的语法糖,但用在 lambda 里会让代码难以阅读,属于反模式。
3. __name__ 永远是 '<lambda>'
>>> f = lambda x: x + 1
>>> f.__name__
'<lambda>'
这意味着如果你需要给函数打上描述性标签(比如日志、调试工具依赖 func.__name__),lambda 就不太合适。这也是为什么装饰器那一章我们会看到 functools.wraps 的存在——它本质上是在修复被包装函数元信息丢失的问题,而 lambda 从一开始就没有元信息可言。
函数作为一等公民:为什么需要 lambda
在 Python 中,函数和其他对象(整数、字符串、列表)地位平等:
- 可以赋值给变量
- 可以作为参数传给其他函数
- 可以从其他函数返回
- 可以存储在数据结构里
这种性质叫 一等公民(first-class citizen)。当函数可以当参数传时,就会产生一类「接收函数作为输入,或者返回函数作为输出」的函数,称为 高阶函数。
最典型的高阶函数就是 map、filter 和 sorted 中的 key 参数。它们的共同特征是:你需要告诉它「对每个元素做什么处理」,而这个处理逻辑本身就是一个函数。用 lambda 可以内联地描述这个逻辑,而不需要提前定义一个命名函数。
操作与实现
lambda 基础语法
最简单的 lambda:
double = lambda x: x * 2
print(double(5)) # 10
带多个参数:
add = lambda x, y: x + y
print(add(3, 4)) # 7
带默认参数(和 def 一样支持):
power = lambda x, n=2: x ** n
print(power(3)) # 9
print(power(3, 3)) # 27
带关键字参数:
greet = lambda name="World": f"Hello, {name}!"
print(greet()) # Hello, World!
print(greet("Alice")) # Hello, Alice!
map:对序列逐一映射
map(function, iterable, ...) 接收一个函数和一个(或多个)可迭代对象,将函数依次作用于每个元素,返回一个迭代器。
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, numbers))
print(squared) # [1, 4, 9, 16, 25]
map 可以接受多个可迭代对象,lambda 对应接收多个参数:
a = [1, 2, 3]
b = [4, 5, 6]
summed = list(map(lambda x, y: x + y, a, b))
print(summed) # [5, 7, 9]
注意 map 返回的是迭代器,需要用 list() 强制求值。在 Python 3 之前它返回的是一个列表,这是个重要的版本差异。
filter:按条件筛选
filter(function, iterable) 接收一个判断函数和一个可迭代对象,返回所有使函数返回 True 的元素构成的迭代器。如果传入 None 作为函数,则过滤掉所有「假值」(0、""、[]、None、False 等)。
numbers = [1, -2, 3, -4, 5, 0]
positive = list(filter(lambda x: x > 0, numbers))
print(positive) # [1, 3, 5]
# 用 None 过滤假值
mixed = [1, "", 0, "hello", None, [], True]
truthy = list(filter(None, mixed))
print(truthy) # [1, 'hello', True]
reduce:累积归约
reduce(function, iterable[, initializer]) 位于 functools 模块(Python 3 中已从内置函数移出),它把一个二元函数「折叠」到序列上,从左到右依次累积计算,最终得到单个结果。
from functools import reduce
numbers = [1, 2, 3, 4, 5]
product = reduce(lambda a, b: a * b, numbers)
print(product) # 120 (1*2*3*4*5)
reduce 的工作原理:先把前两个元素传给函数,把结果和第三个元素传给函数,依此类推。可以用显式循环重写:
# reduce(lambda a, b: a * b, [1,2,3,4,5]) 等价于:
result = 1
for n in [1, 2, 3, 4, 5]:
result = result * n
带初始值的版本:
from functools import reduce
strings = ["Hello", " ", "World"]
concat = reduce(lambda a, b: a + b, strings, "")
print(concat) # Hello World
sorted 的 key 参数:自定义排序
sorted(iterable, key=None, reverse=False) 的 key 参数接收一个函数,该函数会被作用于每个元素,然后根据函数返回值排序。这是 key 最常见的使用场景——用 lambda 提取排序依据。
students = [("Alice", 88), ("Bob", 95), ("Charlie", 72)]
# 按成绩降序排列
by_score = sorted(students, key=lambda s: s[1], reverse=True)
print(by_score) # [('Bob', 95), ('Alice', 88), ('Charlie', 72)]
# 按姓名字母顺序
by_name = sorted(students, key=lambda s: s[0])
print(by_name) # [('Alice', 88), ('Bob', 95), ('Charlie', 72)]
对于对象列表,key 同样适用:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"{self.name}({self.age})"
people = [Person("Alice", 30), Person("Bob", 25), Person("Charlie", 35)]
by_age = sorted(people, key=lambda p: p.age)
print([str(p) for p in by_age]) # ['Bob(25)', 'Alice(30)', 'Charlie(35)']
lambda 与 def 的对比
| 维度 | lambda | def |
|---|---|---|
| 语法形式 | 表达式(可内联) | 语句(需独立定义) |
| 函数体 | 只能含单个表达式 | 任意语句块 |
| 命名 | 无(__name__ 为 '<lambda>') | 有(__name__ 为定义的名字) |
| 文档字符串 | 不支持 | 支持 """...""" |
| 调试 | 困难(栈帧显示 <lambda>) | 容易(显示函数名) |
| 适用场景 | 简短的、单次使用的映射/过滤逻辑 | 复杂的、可复用的逻辑 |
一个实用的判断标准:如果你的 lambda 超过一行逻辑(比如需要 if/else 分支)或者超过 80 个字符,就应该改写成 def。可读性永远优先于「看起来简洁」。
# ❌ 过度使用 lambda,可读性差
result = sorted(data, key=lambda x: (x['last'], x['first']) if x.get('last') else x['first'])
# ✅ 改写成命名函数,清晰得多
def sort_key(person):
if person.get('last'):
return (person['last'], person['first'])
return person['first']
result = sorted(data, key=sort_key)
列表推导式 vs lambda + map/filter
Python 鼓励用**列表推导式(list comprehension)**替代 map + lambda 或 filter + lambda,因为推导式更符合 Python 的语法直觉,且通常略快:
# 三种写法效果相同
nums = [1, 2, 3, 4, 5]
# map + lambda
squared_map = list(map(lambda x: x ** 2, nums))
# 列表推导式(推荐)
squared_comp = [x ** 2 for x in nums]
print(squared_map == squared_comp) # True
但 map 和 filter 并非毫无用处:当处理可迭代对象而非列表时(比如文件行、数据库查询结果),map 和 filter 的惰性求值特性可以避免一次性把所有数据加载到内存,这是列表推导式做不到的。同样,当配合 None 做过滤时,filter(None, iterable) 比推导式更简洁。
易错点与陷阱
陷阱一:在 lambda 里使用 = 赋值
很多初学者会尝试在 lambda 里写类似 lambda x: (y = x * 2, y + 1) 的代码,期望中间结果 y 能被后续表达式引用。这在 Python 3.8 之前会导致 SyntaxError,3.8 之后可以用 walrus operator := 绕过去,但仍然不推荐:
# ❌ Python 3.7 及更早版本直接 SyntaxError
lambda x: (y = x * 2, y + 1)
# ⚠️ Python 3.8+ 可以,但可读性极差
f = lambda x: ((y := x * 2), y + 1)[1]
print(f(5)) # 11
正确做法是直接展开成普通函数,或者把表达式内联进去:
# ✅ 直接用表达式
f = lambda x: x * 2 + 1
# ✅ 用 def,语义清晰
def f(x):
y = x * 2
return y + 1
陷阱二:lambda 闭包捕获的是变量引用,不是值
这是 Python 闭包的经典坑,在 lambda 里尤其隐蔽。看下面这个例子:
funcs = [lambda x: x * i for i in range(4)]
print([f(10) for f in funcs]) # [30, 30, 30, 30] 而不是 [0, 10, 20, 30]
原因:lambda 体内的 i 是一个自由变量,它在执行时查找的是外层作用域的当前值,而不是定义时的值。循环结束后 i 已经是 3,所以所有 lambda 都乘以 3。
修复方法——用默认参数捕获当时的值:
funcs = [lambda x, i=i: x * i for i in range(4)]
print([f(10) for f in funcs]) # [0, 10, 20, 30]
默认参数在函数定义时求值,因此 i=i 会把循环当时的 i 值绑定到 lambda 的局部参数上,不再受外层作用域影响。这个技巧同样适用于 def 定义的函数。
陷阱三:误以为 lambda 比 def 更快
lambda 和 def 在运行时性能上几乎没有任何区别——它们生成的都是 function 字节码对象,调用开销完全相同。lambda 的唯一优势是书写便捷、适合内联,不应该用「性能」来论证是否使用 lambda。相反,过度使用 lambda 会降低代码可读性,反而增加维护成本。在实际工程中,如果一个 lambda 需要你花超过 10 秒钟才能看懂它在做什么,那就说明它应该被改写成带名字的 def 函数,并在函数上方加一行文档字符串说明其意图。
性能与最佳实践对比
在实际工程中,选择 lambda 还是 def 不应仅基于语法简洁度,而应综合考虑可读性、调试便利性与性能特征。
lambda 的性能特征:lambda 与等效的 def 函数在字节码层面几乎一致——CPython 对两者的编译策略相同,都生成一个函数对象(<lambda> vs <module> 内部函数)。用 dis 模块反汇编可见,lambda 的字节码指令序列与等效 def 基本相同,只有函数名称字段有差异(co_name 为 '<lambda>')。
import dis
f = lambda x: x * 2
g = lambda x: x * 2
print(f == g) # True:两个独立 lambda 对象,但功能相同
print(f is g) # False:不同对象
def h(x): return x * 2
print(dis.compare_code(f.__code__, h.__code__)) # 指令序列相同
何时该用 lambda:
- 需要临时传递一个小函数作为参数(如
sorted的key、filter的谓词) - 函数体只有一行表达式,且逻辑简单到一眼能看懂
- 不需要命名、不会复用、不会递归
何时不该用 lambda:
- 逻辑复杂需要多行——改用 def,或在 lambda 内调用辅助函数
- 需要调试——lambda 的
__name__是<lambda>,栈追踪难以辨认 - 需要类型注解——lambda 不支持注解,可读性差
- 需要文档字符串——lambda 无法附带 docstring
记住一条原则:可读性永远优先于简洁性。如果你的同事(或三个月后的你)看到这段代码需要停顿思考,那就应该写成 def。
易错点与陷阱
陷阱一:lambda 不能包含语句
lambda 的限制源于其单表达式的设计。以下写法全部非法:
# ❌ 赋值语句
bad = lambda x: (y := x + 1, y * 2) # 虽然walrus运算符在3.8+可用,但不推荐
# ❌ 循环
bad = lambda x: [print(i) for i in range(x)] # 这是列表推导式,不是循环语句
# ❌ 条件分支多行
bad = lambda x:
if x > 0:
return "positive"
else:
return "negative"
想表达复杂逻辑时,老老实实用 def。lambda 只适合那些”一行说清楚”的场景。
陷阱二:late binding 闭包陷阱在 lambda 中同样存在
第 2 篇讲过的闭包陷阱,lambda 一样会中招。考虑这个常见错误:
# 错误:所有函数都返回最后一个 x 的值
funcs = [lambda x, i=i: x ** i for i in range(5)] # 正确:用默认参数捕获
print([f(2) for f in funcs]) # [1, 2, 4, 8, 16]
# 错误写法(闭包陷阱)
funcs_bad = [lambda x: x ** i for i in range(5)]
print([f(2) for f in funcs_bad]) # [16, 16, 16, 16, 16]:i 是自由变量,延迟绑定
这与普通函数中的闭包行为完全一致。解决方案也是同一个:用默认参数值固化当前迭代状态。
陷阱三:map/filter 返回迭代器而非列表
Python 3 中,map() 和 filter() 返回的是迭代器对象,而不是列表。这意味着:
- 只能遍历一次(消费后即空)
- 惰性求值,按需计算
- 内存效率更高,尤其对大数据集
squares = map(lambda x: x ** 2, range(10))
print(type(squares)) # <class 'map'>
print(list(squares)) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(list(squares)) # []:迭代器已耗尽
如果需要列表,显式调用 list();如果需要保留数据,考虑用列表推导式替代。
小结
lambda 是 Python 中创建匿名函数的一行表达式,它与 def 生成同一种对象(function),但在语法约束上有明显限制:只能包含单个表达式,不能有赋值语句,也不能有多行代码块。这些限制恰好定义了它的适用边界——当你需要一个简短的、只调用一次的逻辑来配合 map、filter、sorted(key=...) 或 reduce 使用时,lambda 非常合适;当逻辑变复杂时,应该毫不犹豫地改用 def。
记住两条经验法则:一是 lambda 不应超过一行(视觉上也不应超过 80 字符),二是不要在 lambda 里尝试用赋值来保存中间结果。满足这两条,lambda 就是你函数式编程工具箱里最趁手的工具之一。同时要注意,lambda 和 def 在运行时性能上没有区别——它们生成的是同一种 function 对象,选择 lambda 的理由应该是代码简洁性,而不是性能。
练习与思考题
-
用
reduce实现一个计算列表最大值的函数find_max(numbers),不借助内置的max()。提示:比较两个数取较大值是一个二元操作。 -
下面这段代码输出什么?解释原因。
fns = [lambda: i for i in range(3)] print([fn() for fn in fns]) -
有一个员工列表
[{"name": "Alice", "salary": 8000}, {"name": "Bob", "salary": 6500}, {"name": "Charlie", "salary": 9200}],用sorted的key参数按薪资从高到低排序,输出排序后的名字列表。 -
讨论:在什么场景下应该优先使用列表推导式而不是
map+lambda?在什么场景下map/filter比列表推导式更合适?