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

Lambda 与推导式 ​

这三个特性有一个共同点:它们能把原本要写好几行才能表达的想法,压缩成一个可读的表达式。用得好,代码会更短更清晰;用得不好,代码会变得难以理解。这一章讲的就是什么时候该用它们,什么时候该收手。

Lambda、推导式和 zip 是三种把常见模式压缩成表达式的工具。它们不是必须掌握的东西,但在 Python 代码里到处都能见到,值得你熟练识别和书写。指导原则是:只在能让意图更清晰时使用它们,而不是仅仅为了更短。

这些工具都是把一次转换表达为表达式(一段能求值得到结果的代码),而不是用命令式循环一步步构建结果。Lambda 让你能就地写一个没有名字的小函数。推导式一次性构建出一个集合。生成器是惰性的(按需产生值,而不是一次性全部生成),所以它在内存中只保留一个元素,而不是整个序列。zip 让你能同步遍历多个序列。这样带来的好处是代码读起来像是"我想要什么",而不是"该怎么拼出来"——前提是你得在单行代码变得难以阅读之前及时停手。

Lambda 函数 ​

lambda 是一个没有名字、只有一个表达式的函数。你用 lambda 关键字来创建它。它真正的用处在于你可以直接在需要的地方内联写出来,而不用先专门定义一个有名字的函数。这正是它在 sorted() 里大显身手的原因。

lambda 是一个匿名的单表达式函数。它可以接受多个参数,但函数体必须是单个表达式,不能是语句。它最主要的用途是作为内联的 key= 参数或回调参数,在这些场景下专门写一个 def 反而会增加不必要的间接层。遇到更复杂的情况,就用 def。

lambda 本质上和 def 产生的是同一种函数对象,只是有三个限制:它没有名字(在traceback——Python 报错时打印的错误信息——里会显示为 <lambda>)、函数体只能是一个表达式,不能包含语句、也不能带 docstring。真正会让你花时间调试的坑是闭包(捕获了外层作用域中变量的函数)里的延迟绑定。在循环里创建的 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 是同一种函数对象,只是少了名字、语句和 docstring,所以就把它用在一行的 key= 上。坑在于延迟绑定:循环里创建的 lambda 读到的是循环变量最终的值,所以要写成 lambda i=i: i 来把它固定住。这个坑几乎每个人都会踩一次。

列表推导式 ​

Python 里最常见的转换操作:拿一个序列,对每个元素做点什么,得到一个新的列表。列表推导式能用一行可读的代码做到这一点:[表达式 for 元素 in 可迭代对象]。你还可以用 if 加一个过滤条件。

列表推导式是"用循环搭建结果"这种模式的简洁替代方案。它通常比等价的、用 .append() 的 for 循环更快,因为 Python 不需要在每一轮都调用一次方法。它的结构是 [表达式 for 元素 in 可迭代对象 if 条件];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]

结构始终是一样的:[表达式 for 元素 in 可迭代对象]。

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列表推导式[表达式 for 元素 in 可迭代对象] 拿一个序列,对每个元素做一件事,返回给你一个新列表。从左到右读一遍,它做的事情一清二楚。这是第一个让我感觉自己是在写 Python,而不是在从别的语言硬翻过来的特性。
Juno列表推导式[表达式 for 元素 in 可迭代对象] 替代了"用循环加 .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 子句是外层循环,第二个是内层循环。它们产生的是一个单一的扁平结果,不是二维结构。如果推导式让人一眼看不明白,就把循环显式地写出来。

子句的顺序和等价嵌套循环的顺序是一致的:第一个 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]

从左到右读:对 matrix 里的每一行,对这一行里的每个元素,把元素纳入结果。

嵌套推导式很快就会让人看糊涂。如果解析它花的时间超过一瞬间,就把循环显式地写出来。

Juno嵌套推导式 一个推导式里放两个 for 子句,就能把一个由列表组成的列表展平成单个列表:[item for row in matrix for item in row]。从左到右读,外层循环在前,正好是你写循环时的那个顺序。要是你的眼睛看着都费劲,那就是提醒你该写真正的循环了。
Juno嵌套推导式 叠加两个 for 子句,第一个是外层循环,第二个是内层:[item for row in matrix for item in row] 把一个二维列表展平成一维。结果是扁平的,不是网格。一旦它不再是一眼就能看明白的程度,显式循环更好。
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 映射到 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](这里比推导式更简洁)
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 到 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 免去了专门写一个具名的键函数。归一化的推导式在不修改原字典的前提下转换了数值。