AI 技术博客
返回首页
Python 基础 21 分钟阅读

lambda 函数:匿名函数与高阶函数基础

Pythonlambda高阶函数函数式编程

引言

这是第四章「函数」的第 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 的对比

维度lambdadef
语法形式表达式(可内联)语句(需独立定义)
函数体只能含单个表达式任意语句块
命名无(__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:

  1. 需要临时传递一个小函数作为参数(如 sorted 的 key、filter 的谓词)
  2. 函数体只有一行表达式,且逻辑简单到一眼能看懂
  3. 不需要命名、不会复用、不会递归

何时不该用 lambda:

  1. 逻辑复杂需要多行——改用 def,或在 lambda 内调用辅助函数
  2. 需要调试——lambda 的 __name__ 是 <lambda>,栈追踪难以辨认
  3. 需要类型注解——lambda 不支持注解,可读性差
  4. 需要文档字符串——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 的理由应该是代码简洁性,而不是性能。

练习与思考题

  1. 用 reduce 实现一个计算列表最大值的函数 find_max(numbers),不借助内置的 max()。提示:比较两个数取较大值是一个二元操作。

  2. 下面这段代码输出什么?解释原因。

    fns = [lambda: i for i in range(3)]
    print([fn() for fn in fns])
  3. 有一个员工列表 [{"name": "Alice", "salary": 8000}, {"name": "Bob", "salary": 6500}, {"name": "Charlie", "salary": 9200}],用 sorted 的 key 参数按薪资从高到低排序,输出排序后的名字列表。

  4. 讨论:在什么场景下应该优先使用列表推导式而不是 map + lambda?在什么场景下 map / filter 比列表推导式更合适?