Lambda 与推导式
这三个特性有一个共同点:它们能把原本要写好几行才能表达的想法,压缩成一个可读的表达式。用得好,代码会更短更清晰;用得不好,代码会变得难以理解。这一章讲的就是什么时候该用它们,什么时候该收手。
Lambda 函数
lambda 是一个没有名字、只有一个表达式的函数。你用 lambda 关键字来创建它。它真正的用处在于你可以直接在需要的地方内联写出来,而不用先专门定义一个有名字的函数。这正是它在 sorted() 里大显身手的原因。
double = lambda x: x * 2
double(5) # 10这等价于:
def double(x):
return x * 2大多数情况下,用 def 就好。lambda 真正的优势只有一个:你可以就地内联写出来,不用给它起名字。这正是它在 sorted()、map() 和 filter() 里好用的原因:
players = [("小明", 87), ("小红", 74), ("小刚", 92)]
sorted(players, key=lambda p: p[1]) # 按分数升序排列
sorted(players, key=lambda p: p[1], reverse=True) # 按分数降序排列如果不用 lambda,你就得专门定义一个有名字的函数,只为了给 key= 用一次。lambda 能把这个意图局部化,一看就懂。
lambda 可以接受多个参数:
add = lambda a, b: a + b
add(3, 4) # 7什么时候用 lambda: 只有当它是用在一个地方的小表达式时才用。如果它变得越来越长,或者你需要复用它,就写一个正规的 def。一个跨越好几个运算符或者需要条件判断的 lambda,通常就是该换成 def 的信号。
lambda 关键字写出来的一个没有名字的单行小函数。它存在的全部意义就是能内联作为 sorted() 的 key=,这样你就不用专门为一件小事定义一个函数了。一旦它超出了一个干净的表达式,我就会写一个真正的 def,心里也更踏实。 列表推导式
Python 里最常见的转换操作:拿一个序列,对每个元素做点什么,得到一个新的列表。列表推导式能用一行可读的代码做到这一点:[表达式 for 元素 in 可迭代对象]。你还可以用 if 加一个过滤条件。
传统写法:
numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
squares.append(n ** 2)列表推导式写法:
squares = [n ** 2 for n in numbers]结构始终是一样的:[表达式 for 元素 in 可迭代对象]。
scores = [87, 42, 96, 55, 71]
scaled = [s * 1.1 for s in scores] # 加上10%的加成
as_grades = [f"{s}/100" for s in scores] # 把每一项格式化输出[表达式 for 元素 in 可迭代对象] 拿一个序列,对每个元素做一件事,返回给你一个新列表。从左到右读一遍,它做的事情一清二楚。这是第一个让我感觉自己是在写 Python,而不是在从别的语言硬翻过来的特性。 用条件过滤
加一个 if 子句,只保留通过测试的元素。结果是一个新列表,只包含条件为 True 的那些元素。
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]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]if condition,只保留通过测试的元素:[x for x in data if x > 0]。任何结果为假值的元素都会被排除在新列表之外。还是你熟悉的那个推导式,只是加了个门卫。 嵌套推导式
你可以嵌套推导式,把一个由列表组成的列表展平成单个列表。从左到右读:对每一行,对这一行里的每个元素,都把这个元素纳入结果。
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 里的每一行,对这一行里的每个元素,把元素纳入结果。
嵌套推导式很快就会让人看糊涂。如果解析它花的时间超过一瞬间,就把循环显式地写出来。
for 子句,就能把一个由列表组成的列表展平成单个列表:[item for row in matrix for item in row]。从左到右读,外层循环在前,正好是你写循环时的那个顺序。要是你的眼睛看着都费劲,那就是提醒你该写真正的循环了。 字典推导式
字典推导式用一个表达式构建出一个字典,思路和列表推导式一样:{key: value for item in iterable}。同样可以用 if 加一个过滤条件,和列表推导式一样。
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}带过滤条件:
passing = {name: score for name, score in score_map.items() if score >= 80}
# {"alice": 87, "carol": 92}words = ["apple", "banana", "cherry"]
word_lens = {word: len(word) for word in words}
# {"apple": 5, "banana": 6, "cherry": 6}{key: value for item in iterable} 一行代码构建出一个字典,形状和列表推导式一样,只是键和值之间多了个冒号。搭配 .items() 可以重塑一个已有的字典,搭配 zip() 可以把两个列表缝合成一个映射。在末尾加个 if,就能只保留你想要的键值对。 集合推导式
集合推导式用一个表达式构建出一个集合,用花括号,没有冒号。因为结果是集合,重复项会自动被去掉。
words = ["apple", "banana", "cherry", "apple"]
unique = {w.lower() for w in words} # {"apple", "banana", "cherry"}当你只关心唯一值、不在意顺序时,就用集合推导式。
{expr for item in iterable},用花括号、没有冒号,构建出一个集合,而集合天生就会免费帮你去掉重复项。所以如果你的任务是"给我唯一的那些",一行代码就能搞定。但别指望它会按某个特定顺序返回结果。 生成器表达式
生成器看起来就像用圆括号而不是方括号的列表推导式。关键区别在于:列表推导式会把整个列表一次性构建到内存里。生成器是一次产生一个值,只在需要的时候才产生。对于大型序列,这样能省下不少内存。
squares_gen = (n ** 2 for n in range(1000000))total = sum(n ** 2 for n in range(1000000)) # sum() 消费这个生成器把生成器直接传给像 sum()、max()、min() 或 any() 这样的函数时,可以省掉多余的一层括号:
total = sum(n ** 2 for n in range(1000)) # 只有一层括号,不是两层对大多数日常代码来说,列表推导式就够用了。当你要处理的是大型数据集,或者把所有东西都保留在内存里会很浪费的流式数据时,就用生成器。
sum() 或 max(),压根不用构建那个列表。 zip()
zip() 把两个或多个序列的元素配对起来,让你能并行遍历它们。它会在最短的那个序列耗尽时停下。这是避免手动管理索引、处理两个相互对应的列表时最干净的方式。
names = ["小明", "小红", "小刚"]
scores = [87, 74, 92]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# 小明: 87
# 小红: 74
# 小刚: 92zip() 会在最短的序列耗尽时停下。如果你的序列长度可能不一致,就用带填充值的 itertools.zip_longest()。
要把一个由配对元组组成的压缩列表还原回两个独立的列表,用 zip(*pairs):
pairs = [("小明", 87), ("小红", 74), ("小刚", 92)]
names, scores = zip(*pairs)
# names = ("小明", "小红", "小刚")
# scores = (87, 74, 92)*pairs 把这个列表拆开成一个个独立的参数,所以 zip(*pairs) 就变成了 zip(("小明", 87), ("小红", 74), ("小刚", 92))。* 运算符的用法在函数那一章有讲解。
zip() 同样是并行遍历多个序列、又不用手动管理索引的干净方式:
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})")zip() 把两个或多个序列配对起来,让你一起遍历它们,不用手动折腾索引。它在最短的那个序列耗尽时停下,所以长度不一致的话,多出来的部分会悄悄丢掉。而 zip(*pairs) 是反过来操作,把一个元组列表拆回两个独立的列表。 map() 和 filter()
map() 和 filter() 是更早期的函数式风格工具,做的事情和推导式一样。你会在旧代码里见到它们,所以了解它们的含义还是值得的。新代码里优先用推导式,对大多数 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():
strings = ["1", "2", "3"]
numbers = list(map(int, strings)) # [1, 2, 3](这里比推导式更简洁)map(func, iterable) 对每个元素运行一个函数;filter(func, iterable) 只保留那些让函数返回真值的元素。它们是完成推导式同样工作的更古老的方式,所以你会在别人的代码里遇到它们。写自己的代码时,对大多数人来说推导式读起来更清楚。 实战演练
过滤出玩家列表中及格的分数,用 sorted 配合 lambda 按分数排名,再用带序号的位置打印出来:
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}")
