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

Lambda 函数和推导式

docs.scrimba.com

这三个特性有一个共同点:它们让你用一个可读的表达式来表达那些原本需要多行代码的思想。使用得当时,它们会使代码更短更清晰。使用不当时,会让代码无法阅读。本章涵盖了何时使用每一个工具以及何时停止。

Lambda 函数、推导式和 zip 是三个工具,可以将常见的模式压缩成表达式。它们不是必须的,但在整个 Python 代码中都能看到,值得学会识别和编写。指导原则:当它们使意图更清晰而不仅仅是更短时,就使用它们。

这些工具都将转换表达为表达式(一段代码,计算结果为一个值),而不是一个逐步构建结果的命令式循环。Lambda 给你一个小的未命名函数内联。推导式一次通过构建集合。生成器是惰性的(它们按需生成值而不是一次性全部),所以在内存中只保存一个项而不是整个序列。zip 同步遍历多个序列。收益是代码读起来像"我想要什么"而不是"如何组装它",只要你在单行变得无法阅读之前停止。

Lambda 函数

Lambda 是一个无名的、单表达式函数。你用 lambda 关键字创建它。它真正的用处在于你可以在需要的地方直接内联编写它,无需预先定义一个命名函数。这就是它与 sorted() 一起有用的原因。

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

Lambda 是与 def 相同的函数对象,有三个限制:它没有名字(它在回溯中显示为 <lambda>,即 Python 在出现错误时打印的错误报告)、其主体是一个表达式所以它不能包含语句、它也没有文档字符串。花费真实调试时间的陷阱是闭包中的延迟绑定(一个函数捕获周围作用域中的变量)。在循环内构建的 lambda 按引用捕获循环变量,所以每个 lambda 都看到该变量的最终值,而不是创建 lambda 时的值:

python
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])        # [2, 2, 2],不是 [0, 1, 2]

funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])        # [0, 1, 2]:i=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 的信号。

JunoLambda 函数 Lambda 是一个微小的单行函数,没有名字,用 lambda 关键字编写。它存在的全部原因是作为 sorted()key= 内联,这样你就不需要为一个任务定义单独的函数。一旦它变得长于一个整洁的表达式,我就写一个真正的 def 并感觉好多了。
JunoLambda 函数 Lambda 是一个匿名单表达式函数,作为一个内联的 key= 或回调很方便,命名的 def 只会增加噪音。主体是一个表达式,不允许语句。任何更复杂的东西,使用 def
JunoLambda 函数def 相同的函数对象,缺少名字、语句和文档字符串,所以保持它们为单行 key=。陷阱是延迟绑定:在循环中构建的 lambda 读取循环变量的最终值,所以写 lambda i=i: i 来固定它。这个陷阱会咬每个人一次。

列表推导式

Python 中最常见的转换:取一个序列,对每个项做某事,得到一个新的列表。列表推导式在一个可读的行中做这个:[expression for item in iterable]。你也可以用 if 添加一个过滤器。

列表推导式是用循环构建模式的简明替代。它们通常比等价的带有 .append()for 循环更快,因为 Python 不会在每次通过时调用一个方法。结构是 [expression for item in iterable if condition]if 子句是可选的。

推导式比调用 .append() 的同等循环更快,所以它是从转换构建列表的习惯用法。它也在其自己的作用域中运行(一个名字可见的区域),这意味着循环变量不会泄漏到周围的代码中:在 [n ** 2 for n in numbers] 之后,名字 n 在推导式外不存在。要注意的陷阱是为真正做工作的地方选择推导式。一旦表达式超过了一个整洁的转换,或者你想添加日志记录或 try 块,显式循环是更好的工具,因为推导式只能包含表达式,永远不能包含语句。

长的方式:

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]    # 格式化每一个
Juno列表推导式[expression for item in iterable] 取一个序列,对每个项做一件事,然后交给你一个新列表。从左到右阅读它,它确切地说出它做什么。这是第一个让我感觉我在写 Python 而不是从另一种语言翻译的 Python 特性。
Juno列表推导式[expression for item in iterable] 替换构建一个循环并 .append() 的模式,这样做读起来更整洁。它产生一个新列表并且不改变任何东西。保持表达式简单,否则你获得的可读性就消失了。
Juno列表推导式.append() 循环快,并且在其自己的作用域中运行,所以循环变量永远不会泄漏出来。底线是:推导式取一个表达式,不是语句,所以一旦你想要一个 try 块或一行日志,显式循环获胜。

用条件过滤

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

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

if 的位置决定了你是过滤还是转换,混淆它们是导致结果长度错误的常见原因。末尾的 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]
Juno用条件过滤 在末尾添加 if condition 以仅保留通过测试的项:[x for x in data if x > 0]。任何返回假的东西都会被排除在新列表之外。同一个推导式,它上面有一个守卫。
Juno用条件过滤 尾随 `if` 过滤,仅保留条件为真的项。这与表达式内部的条件不同:[x if x > 0 else 0 for x in data] 重写值而不是丢弃它们。在写之前知道你想要哪一个。
Juno用条件过滤 末尾的 `if` 过滤并改变长度;前面的条件映射并保持它。混淆这两个,你会发布一个大小错误的结果。堆叠的尾随 `if` 子句用 `and` 链接。

嵌套推导式

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

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

子句顺序按与等价嵌套循环相同的顺序读取:第一个 for 是外层循环,第二个是内层,后面的子句可以使用前面子句绑定的名字。这在展平时赚钱的地方;在真正的组合网格(来自两个序列的每一对)中伤害的地方,对于这个情况,itertools.product 读起来远清晰于双 for。保持代码库完整的规则:如果解析推导式需要超过一秒,显式循环是更好的文档,所以写那个代替。

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]

从左到右阅读它:对于矩阵中的每一行,对于行中的每一项,包括该项。

嵌套推导式很快就会变得混乱。如果解析需要超过一刻,显式地写循环。

Juno嵌套推导式 一个推导式中的两个 `for` 子句将列表的列表展平为单个平面列表:[item for row in matrix for item in row]。从左到右阅读,外层循环第一,完全按照你会写循环的顺序。如果你的眼睛在它上面卡住了,那是你的提示,改为写真正的循环。
Juno嵌套推导式 堆叠两个 `for` 子句,第一个是外层循环,第二个是内层:[item for row in matrix for item in row] 将 2D 列表展平为一个。结果是平面的,不是网格。当它停止一眼可读时,显式循环更好。
Juno嵌套推导式 第一个 `for` 是外层循环,后面的子句可以使用前面子句的名字。对于展平很好,对于每一对组合很差,`itertools.product` 读起来更清晰。如果解析它需要超过一秒,循环是更好的文档。

字典推导式

字典推导式在一个表达式中构建一个字典,使用与列表推导式相同的思想:{key: value for item in iterable}。用 if 添加过滤器,与列表推导式相同。

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

两件事在真实代码中咬人。首先,每个键必须是可哈希的(Python 可以减少到固定查找数的值,这意味着它永远不会改变,所以字符串和数字和元组符合但列表和字典不符合)。产生列表的键表达式会引发 TypeError。其次,重复键不会出错:如果循环产生相同的键两次,较后的值静默地覆盖较早的值,当你从不是真正唯一的来源构建字典时,这是丢弃数据的安静来源。当你只想合并两个现有字典时,| 运算符(字典合并形式,Python 3.9 及以上)比推导式更清晰地表达了意图。

python
names = ["alice", "bob", "carol"]
scores = [87, 74, 92]

score_map = {name: score for name, score in zip(names, scores)}
# {"alice": 87, "bob": 74, "carol": 92}

带有过滤器:

python
passing = {name: score for name, score in score_map.items() if score >= 80}
# {"alice": 87, "carol": 92}
python
words = ["apple", "banana", "cherry"]
word_lens = {word: len(word) for word in words}
# {"apple": 5, "banana": 6, "cherry": 6}
Juno字典推导式{key: value for item in iterable} 在一行中构建字典,形状与列表推导式相同,在键和值之间有冒号。将其与 .items() 配对以重塑你已拥有的字典,或与 zip() 配对以将两个列表缝合成一个映射。在末尾添加 `if` 以仅保留你想要的对。
Juno字典推导式{key: value for item in iterable} 从任何对源创建字典。两个日常源是现有字典上的 .items() 和两个列表上的 zip()。注意重复的键:最后一个获胜,静默地。
Juno字典推导式 键必须是可哈希的(没有列表),重复的键不会出错,较后的值静默地覆盖,当你的源不唯一时这会吃掉数据。所以在你信任计数之前检查唯一性。要合并两个字典,| 比推导式说得更清楚。

集合推导式

集合推导式在一个表达式中构建一个集合,带有花括号和没有冒号。因为结果是一个集合,重复会自动移除。

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

结果是一个 set,所以两件事遵循列表版本不给你的。来自表达式的重复值自动折叠为一个,这就是重点:它是在单个表达式中去重转换的最清晰方式。成本是集合没有顺序,所以你不能依赖项以任何特定序列出来。成员也必须是可哈希的,与字典键适用的规则相同:Python 可以减少到固定查找数的值,这排除了列表。

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

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

Juno集合推导式{expr for item in iterable} 带有花括号和没有冒号构建一个集合,一个集合免费扔掉重复。所以如果你的工作是"给我唯一的那些",这在一行中做它。不过不要指望任何特定的顺序回来。
Juno集合推导式{expr for item in iterable} 产生一个 `set`,边去重。当你想要一个从转换构建的唯一集合且顺序不重要时,使用它。花括号和没有冒号,这是它与字典推导式之间的唯一区别。
Juno集合推导式 集合自动丢弃重复,这是选择它胜过列表推导式的全部原因。交易是没有顺序可依赖,成员必须是可哈希的,所以列表内没有。最佳契合:在一个表达式中去重转换。

生成器表达式

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

生成器表达式产生一个迭代器,不是集合。它懒惰地计算值:下一个值仅在请求时产生。当结果被 sum()max()any() 之类的函数立即消费时,这最有价值,所以首先建立完整列表没有意义。

生成器懒惰地产生值(一次一个,仅在某物请求下一个时),所以它的内存保持平面不管输入有多大:它永远不会一次保存超过当前值,列表推导式一次保存每个元素。这使它对从大型或流式源直接馈入 sum()max()any() 的正确调用。要记住的失败模式是生成器是单用:一旦某物迭代它到末尾,它就耗尽了并产生不了更多,所以对同一生成器的第二个循环运行零次且不给出错误。如果你需要两次遍历数据,一次构建列表并迭代那个。

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))   # 一组括号,不是两组

对于大多数日常代码,列表推导式很好。当你处理大数据集或流式数据时使用生成器,其中在内存中保存所有东西会很浪费。

Juno生成器表达式 生成器看起来像一个列表推导式,括号而不是方括号,但它一次生成一个值而不是提前构建整个列表。对于一个巨大的序列,这节省了大量内存。整洁的情况:直接将一个放入 sum()max() 并完全跳过构建列表。
Juno生成器表达式 生成器表达式返回一个懒惰迭代器:它仅在询问时计算下一个值,所以没有东西在内存中构建完整列表。最好当结果直接馈入 sum()max()any()。记住它是单用,一旦消费它就是空的。
Juno生成器表达式 懒惰且内存中平面不管输入有多大,所以对于馈入 sum()any() 的大型或流式源它是正确的工具。抓住:它是单用,对耗尽生成器的第二个循环运行零次且没有错误。需要两次通过?一次构建列表。

zip()

zip() 将两个或更多序列的项配对在一起,所以你可以通过它们平行循环。它在最短序列处停止。这是避免在两个列表相互对应时管理索引的干净方式。

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

zip() 按步骤遍历其输入并在最短输入耗尽的那一刻停止。那个截断是设计周围的陷阱:将 1000 条记录列表与 999 条列表配对,你静默地丢失最后一条记录,没有错误,没有警告。当长度应该匹配时,zip(seq_a, seq_b, strict=True) (Python 3.10 及以上)在它们不同时引发而不是丢弃数据,当它们允许不同时 itertools.zip_longest 填充间隙。另一个值得知道的技巧:zip(*rows) 转置,将行列表变成列列表,因为 * 将外层列表解包为单独的参数。

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})")
Junozip()zip() 配对两个或更多序列,这样你可以循环它们一起,没有索引杂耍。它在最短处停止,所以不匹配的长度静默地丢失额外的。且 zip(*pairs) 反向运行它,将元组列表分裂回单独的列表。
Junozip()zip() 返回一个懒惰元组迭代器,平行通过其输入步进。它在最短处停止,所以较长序列得到截断不出一声。`itertools.zip_longest()` 填充间隙当长度合法地不同时,且 `zip(*pairs)` 解开拉链。
Junozip() 懒惰、平行且在最短输入处停止,当长度意图匹配时静默地丢弃数据。传递 `strict=True` 使不匹配引发代替。`zip(*rows)` 是你的转置,从行一次调用中得列。

map() 和 filter()

map()filter() 是做推导式做的东西的较老函数式样式工具。你会在较老的代码中看到它们,所以值得知道它们是什么意思。优先考虑新代码的推导式;它们对大多数 Python 开发人员更可读。

map(func, iterable) 返回一个懒惰迭代器,将 func 应用于每一项。filter(func, iterable) 返回一个懒惰迭代器,其中 func 为真的项。两者都先于推导式。在新代码中优先推导式;当你已经有做你需要的命名函数时使用 map()

两者都返回懒惰迭代器,不是列表,所以当你想现在要值时在 list() 中包装它们。它们一对一映射到你已经知道的推导式上:map(f, it)(f(x) for x in it),且 filter(pred, it)(x for x in it if pred(x))。那个等价是决定规则。用内联 lambda,推导式读得更好且是现代默认。用已经做工作的命名函数,list(map(int, strings)) 读作"映射 int 在字符串上"且是更干净选择,所以唯一的真实调用是你是否已经有一个函数在手。

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](比推导式更干净)
Junomap() 和 filter()map(func, iterable) 在每个项上运行一个函数;filter(func, iterable) 仅保留函数来回真的项。它们是做推导式做的更老方式,所以你会在其他人的代码中遇到它们。对于你自己的,推导式对大多数人读得更清楚。
Junomap() 和 filter()map() 转换每个项,filter() 保留真的,两个懒惰所以包装在 list() 中看结果。推导式是新代码的默认。唯一 map() 获胜的地方是一个你已经拥有的命名函数:`map(int, strings)` 读比推导式更整洁。
Junomap() 和 filter()map(f, it)(f(x) for x in it)filter(pred, it)(x for x in it if pred(x)),两个懒惰。所以选择纯粹是可读性:用内联 lambda,推导式获胜;用一个已经命名的函数,`map(int, strings)` 获胜。

实践中

过滤玩家列表到通过得分,用 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-to-name 查找字典,并在一次传递中各自收集排序的名字:

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"活跃管理员: {[u['name'] for u in active_admins]}")
print(f"所有活跃: {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("前 2 特性:")
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}")

zip 配对两个列表而不构建中间元组。字典推导式在一个表达式中构建映射。排序 lambda 避免一个命名 key 函数。规范化推导式转换值而不变异原始字典。