Skip to content
This page has been auto-translated and may contain errors.View in English

Lambda 和推导式

这三个特性有一个共同点:它们让你用一个可读的表达式表达原本需要几行代码才能表达的想法。用得好,可以使代码更简短、更清晰;用得不好,则会让代码难以阅读。本章涵盖了何时使用每一个,以及何时不要使用。

Lambda、推导式和 zip 是三个将常见模式压缩为表达式的工具。它们不是必需的,但在 Python 代码中随处可见,值得认识并能够熟练书写。指导原则是:在它们让意图更清晰时使用它们,而不仅仅是为了更简短。

Lambda 表达式在运行时创建匿名函数对象。推导式编译为优化过的字节码,在外层帧中无需 for 循环即可构建集合。生成器是惰性的:它们按需产生值,而无需具现整个序列。zip 返回一个元组迭代器,惰性地消费输入的可迭代对象。这三者都体现了将转换表达为表达式而非命令式循环的主题。

Lambda 函数

lambda 是一个无名的、只有一个表达式的函数。你用 lambda 关键字创建它。它真正的用处在于,你可以在需要它的地方就地写出来,而无需先定义一个具名函数。这正是它与 sorted() 一起使用时的实用之处。

lambda 是一个匿名的单表达式函数。它可以接受多个参数,但函数体必须是单个表达式,而不是语句。它的主要用途是作为内联的 key= 或回调参数,使用完整的 def 会带来不必要的间接性。对于更复杂的情况,请使用 def

lambda args: expression 编译为一个代码对象并创建一个函数对象,与 def 相同,只是它没有名字(在回溯中显示为 <lambda>),不能包含语句,也不支持文档字符串或注解。Lambda 参与闭包:自由变量从封闭作用域中捕获。常见陷阱:循环中的 lambda i: i 通过引用而非值捕获 i;使用 lambda i=i: i 在创建时绑定值。

python
double = lambda x: x * 2
double(5)   # 10

这等价于:

python
def double(x):
    return x * 2

在大多数情况下,使用 def。Lambda 有一个真正的优势:你可以在需要它们的地方内联编写它们,而无需命名。这正是它们在 sorted()map()filter() 中有用的原因:

python
players = [("小明", 87), ("小红", 74), ("小华", 92)]

sorted(players, key=lambda p: p[1])              # 按分数排序(升序)
sorted(players, key=lambda p: p[1], reverse=True)  # 按分数排序(降序)

如果没有 lambda,你将不得不为 key= 参数定义一个具名函数。Lambda 让意图保持在局部并清晰可见。

Lambda 可以接受多个参数:

python
add = lambda a, b: a + b
add(3, 4)   # 7

何时使用 lambda: 仅当它是一个用于单一场景的简单表达式时。如果它变得复杂,或者你需要重用它,请编写一个正规的 def。一个跨越多个运算符或需要条件判断的 lambda 通常是切换到 def 的信号。

列表推导式

Python 中最常见的转换:取一个序列,对每个元素做一些操作,得到一个新列表。列表推导式可以在一行可读的代码中完成此操作:[expression for item in iterable]。你也可以使用 if 添加过滤条件。

列表推导式是循环构建模式的简洁替代品。它们编译为优化过的字节码,通常比等价的带 .append()for 循环更快。结构是 [expression for item in iterable if condition]if 子句是可选的。

列表推导式编译为专用字节码中的 LIST_APPEND 循环,比 Python 层循环中重复调用 list.append() 更快。它们在 Python 3 中创建新的作用域(与 Python 2 不同),因此循环变量不会泄漏。嵌套推导式从左到右、从上到下执行:[expr for x in a for y in b] 等价于以 x 作为外层循环的嵌套 for 循环。

冗长的写法:

python
numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
    squares.append(n ** 2)

列表推导式写法:

python
squares = [n ** 2 for n in numbers]

结构始终相同:[expression for item in iterable]

python
scores    = [87, 42, 96, 55, 71]
scaled    = [s * 1.1 for s in scores]       # 应用 10% 的加成
as_grades = [f"{s}/100" for s in scores]    # 格式化每一项

使用条件进行过滤

添加一个 if 子句以仅包括通过测试的项目。结果是一个新列表,其中只包含条件为 True 的项。

推导式中的 if 子句是过滤器,而不是 if/else。它对每个项目运行一次,只包含条件为真的项目。对于条件转换(根据条件将一个值映射到另一个值),请在主表达式中使用三元表达式。

if 过滤器与输出中的条件表达式不同。[x for x in data if x > 0] 进行过滤。[x if x > 0 else 0 for x in data] 进行映射(将值钳制为零)。你可以将两者结合:[x * 2 for x in data if x > 0]。多个 if 子句通过隐式 and 链接。

python
numbers  = [1, 2, 3, 4, 5, 6, 7, 8]
evens    = [n for n in numbers if n % 2 == 0]    # [2, 4, 6, 8]
odds     = [n for n in numbers if n % 2 != 0]    # [1, 3, 5, 7]
python
scores   = [87, 42, 96, 55, 71, 38]
passing  = [s for s in scores if s >= 60]    # [87, 96, 71]
failing  = [s for s in scores if s < 60]     # [42, 55, 38]

嵌套推导式

你可以嵌套推导式,将列表的列表展平为单个列表。从左到右阅读:对于每一行,对于该行中的每一项,包含该项。

嵌套推导式从左到右执行。第一个 for 子句是外层循环,第二个是内层循环。它们产生单一的扁平结果,而不是二维结构。如果推导式难以一眼看懂,请显式地写出循环。

嵌套推导式作为嵌套循环执行,第一个 for 是最外层。每个循环变量的作用域可供后续子句使用。对于笛卡尔积,itertools.product 通常更清晰。关键的可读性规则:如果解析推导式需要超过一秒钟,显式的循环形式是更好的文档。

python
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat   = [item for row in matrix for item in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

从左到右阅读:对于 matrix 中的每一行,对于行中的每一项,包含 item。

嵌套推导式很容易令人混淆。如果需要片刻才能解析,请显式地写出循环。

字典推导式

字典推导式使用一个表达式构建字典,理念与列表推导式相同:{key: value for item in iterable}。像列表推导式一样,使用 if 添加过滤条件。

字典推导式从任何产生键值对的可迭代对象创建新字典。语法是 {key_expr: val_expr for item in iterable if condition}。循环中的重复键会静默地使用最后一个值。对现有字典执行 .items() 是字典推导式最常见的源可迭代对象。

字典推导式编译为专用的 MAP_ADD 字节码,类似于列表推导式的 LIST_APPEND。它们在 Python 3 中创建新的作用域。键表达式必须生成可哈希值;如果键表达式产生重复,后面的值会静默地获胜。对于有序合并语义,| 运算符(Python 3.9+)比推导式更简洁。

python
names  = ["小明", "小红", "小华"]
scores = [87, 74, 92]

score_map = {name: score for name, score in zip(names, scores)}
# {"小明": 87, "小红": 74, "小华": 92}

带过滤器:

python
passing = {name: score for name, score in score_map.items() if score >= 80}
# {"小明": 87, "小华": 92}
python
words     = ["apple", "banana", "cherry"]
word_lens = {word: len(word) for word in words}
# {"apple": 5, "banana": 6, "cherry": 6}

集合推导式

集合推导式使用一个表达式构建集合,使用花括号且没有冒号。由于结果是一个集合,重复项会被自动删除。

集合推导式使用 {expression for item in iterable} 并产生一个 set。它们会自动去重。当你需要从转换中构建一个唯一集合,且顺序无关紧要时使用它们。

集合推导式编译为 SET_ADD 字节码。结果是一个无序集合:表达式产生的重复值会被静默合并。集合推导式不如列表或字典推导式常见,但它是用一个表达式产生去重转换的简洁方式。

python
words   = ["apple", "banana", "cherry", "apple"]
unique  = {w.lower() for w in words}    # {"apple", "banana", "cherry"}

当你需要唯一值且不关心顺序时,使用集合推导式。

生成器表达式

生成器看起来像列表推导式,但用圆括号而不是方括号。关键区别在于:列表推导式一次性在内存中构建整个列表,而生成器一次产生一个值,只在需要时产生。对于大型序列,这会使用少得多的内存。

生成器表达式产生一个迭代器,而不是一个集合。它惰性地计算值:下一个值只在被请求时才产生。当结果被 sum()max()any() 等函数立即消费时,这一点最有价值,因为没必要先构建完整的列表。

生成器表达式编译为一个代码对象并返回一个生成器对象。值通过 __next__ 惰性地产生,使内存使用量保持在 O(1),与输入大小无关。它们参与迭代器协议,并可以链接。当直接传递给接受可迭代对象的函数时,可以省略外层括号。生成器在耗尽后不能重用;如果你需要多次迭代,请具现为列表。

python
squares_gen = (n ** 2 for n in range(1000000))
python
total = sum(n ** 2 for n in range(1000000))   # sum() 消费生成器

当直接将生成器传递给 sum()max()min()any() 等函数时,你可以省去额外的括号:

python
total = sum(n ** 2 for n in range(1000))   # 一对括号,而不是两对

对于大多数日常代码,列表推导式就足够了。当你处理大型数据集或流式数据,把所有数据保存在内存中会造成浪费时,请使用生成器。

zip()

zip() 将两个或多个序列中的项配对在一起,以便你可以并行地遍历它们。它在最短的序列处停止。当两个列表相互对应时,它是避免手动管理索引的简洁方式。

zip() 返回一个惰性的元组迭代器,同步地消费其输入可迭代对象。它在最短的输入处停止:较长的序列会被静默截断。对于长度可能不同的序列,itertools.zip_longest() 会用指定的值填充较短的序列。

zip() 返回一个 zip 对象,这是一个惰性的迭代器,同时对每个输入迭代器调用 next()。当任何迭代器引发 StopIteration 时它就停止。所有输入都是惰性消费的:zip() 本身分配 O(1) 内存,与输入大小无关。zip(*iterable) 是标准的转置操作;* 将外层可迭代对象解包为单独的参数。

python
names  = ["小明", "小红", "小华"]
scores = [87, 74, 92]

for name, score in zip(names, scores):
    print(f"{name}: {score}")
# 小明: 87
# 小红: 74
# 小华: 92

zip() 在最短的序列处停止。如果你的序列可能长度不同,请使用 itertools.zip_longest() 并指定填充值。

要从配对的列表转换回两个单独的列表,使用 zip(*pairs)

python
pairs  = [("小明", 87), ("小红", 74), ("小华", 92)]
names, scores = zip(*pairs)
# names = ("小明", "小红", "小华")
# scores = (87, 74, 92)

* 在这里做什么?

*pairs 将列表解包为单独的参数:zip(*pairs) 变成 zip(("小明", 87), ("小红", 74), ("小华", 92))* 运算符在函数章节中有介绍。

zip() 也是并行迭代多个序列而无需手动管理索引的简洁方法:

python
before = [10, 20, 30]
after  = [15, 18, 35]

for b, a in zip(before, after):
    change = a - b
    print(f"{b} -> {a} ({'+' if change >= 0 else ''}{change})")

map() 和 filter()

map()filter() 是较老的函数式风格工具,它们做的事情和推导式一样。你会在旧代码中看到它们,所以了解它们的含义是值得的。新代码中优先使用推导式;对于大多数 Python 开发者来说,推导式更具可读性。

map(func, iterable) 返回一个惰性迭代器,将 func 应用于每个项目。filter(func, iterable) 返回一个惰性迭代器,其中包含 func 为真的项目。两者都早于推导式出现。新代码中优先使用推导式;当你已经有一个完成所需功能的具名函数时,使用 map()

map()filter() 在 Python 3 中返回惰性迭代器(而非列表)。map(f, it) 等价于 (f(x) for x in it)filter(pred, it) 等价于 (x for x in it if pred(x))。对于具名函数,list(map(int, strings)) 是惯用的,因为它读作"将 int 映射到 strings 上";等效的推导式 [int(s) for s in strings] 同样有效。

python
numbers = [1, 2, 3, 4, 5]

list(map(lambda x: x ** 2, numbers))         # [1, 4, 9, 16, 25]
list(filter(lambda x: x % 2 == 0, numbers))  # [2, 4]

优先使用推导式;它们对大多数 Python 开发者来说更具可读性。当你已经有一个具名函数时,使用 map()

python
strings = ["1", "2", "3"]
numbers = list(map(int, strings))   # [1, 2, 3] (在这里比推导式更简洁)

实战应用

将玩家列表过滤为及格分数,使用 sorted 和 lambda 按分数排序,然后使用枚举位置打印:

python
players = [
    {"name": "小明", "score": 87},
    {"name": "小红", "score": 42},
    {"name": "小华", "score": 96},
    {"name": "小刚", "score": 55},
]

passing   = [p for p in players if p["score"] >= 60]
ranked    = sorted(passing, key=lambda p: p["score"], reverse=True)
score_map = {p["name"]: p["score"] for p in ranked}

for i, (name, score) in enumerate(score_map.items(), start=1):
    print(f"{i}. {name}: {score}")

过滤用户列表以获取活跃的管理员,构建 id 到名称的查找字典,并通过一次遍历各自收集已排序的名称:

python
raw_users = [
    {"id": 1, "name": "小明", "role": "admin", "active": True},
    {"id": 2, "name": "小红", "role": "user",  "active": False},
    {"id": 3, "name": "小华", "role": "admin", "active": True},
    {"id": 4, "name": "小刚", "role": "user",  "active": True},
]

active_admins = [u for u in raw_users if u["active"] and u["role"] == "admin"]
id_map        = {u["id"]: u["name"] for u in raw_users}
names         = sorted(u["name"] for u in raw_users if u["active"])

print(f"Active admins: {[u['name'] for u in active_admins]}")
print(f"All active: {names}")

使用 zip 将特征名称与重要性得分配对,构建字典推导式,使用 lambda 排序,并在第二个推导式中归一化值:

python
feature_names = ["age", "income", "score", "tenure"]
importances   = [0.12, 0.34, 0.28, 0.26]

feat_dict = {f: i for f, i in zip(feature_names, importances)}
top_feats = sorted(feat_dict.items(), key=lambda x: x[1], reverse=True)[:2]

print("Top 2 features:")
for name, score in top_feats:
    print(f"  {name}: {score:.2f}")

# 归一化使总和为 1.0(这里的值已经总和为 1,但作为模式展示)
total      = sum(feat_dict.values())
normalised = {k: round(v / total, 4) for k, v in feat_dict.items()}
print(f"Normalised: {normalised}")

zip 配对两个列表,无需构建中间元组。字典推导式用一个表达式构建映射。排序 lambda 避免了具名 key 函数。归一化推导式在不改变原始字典的情况下转换值。