最近在整理代码时,我又一次感受到了 Python 推导式(Comprehensions)的精妙之处。它不仅仅是一种“语法糖”,我更愿意将其看作是一种思维方式的凝练表达。今天,我们就来重新梳理一下这个我们既熟悉又可能有些陌生的特性。

在编程中,我们经常会遇到一个基础性的需求:根据一个已有的序列,通过某种运算,生成一个新的列表。比如,我们想创建一个包含 0 到 9 平方的列表。最直观、最基础的写法,自然是借助 for 循环:

squares = []
for i in range(10):
    squares.append(i * i)
 
# 得到 [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

这段代码清晰明了,没有任何理解上的困难。但仔细品味,会觉得它有些啰嗦。我们定义了一个空列表 squares,然后在循环体内部不断地调用 append 方法去“填充”它。整个过程被拆分成了“初始化”、“循环”、“填充”三个步骤。然而,我们的核心意图其实只有一个:创建一个列表

既然核心意图如此明确,有没有办法让代码的表达也同样直接呢?Python 的列表推导式(List Comprehension)正是为此而生。上面的代码,我们可以用一行来等价实现:

squares = [i * i for i in range(10)]

这个写法的妙处在于,它将“做什么”(i * i,对元素进行平方运算)和“从哪里做”(for i in range(10),遍历 0 到 9)这两个核心信息,紧凑地封装在了一对方括号 [] 内部。方括号本身就清晰地表明了我们的最终目标是创建一个列表。代码即意图,这让我觉得非常优雅。

一个很自然的想法是:既然列表可以这样做,那么其他常见的数据结构,比如字典(dict),是否也能享受到这种便利呢?

答案是肯定的。我们只需稍作变通。对于字典,它的基本构成单元是键值对(key-value pair)。因此,在推导式中,我们需要提供的就不能再是单个元素的表达式,而应该是一个“键: 值”格式的表达式。

比如,我想创建一个从 0 到 9 的数字及其字符串形式的映射字典,可以这样写:

mapping = {key: str(key) for key in range(10)}
 
# 得到 {0: '0', 1: '1', 2: '2', ..., 9: '9'}

可以看到,其结构与列表推导式几乎完全一致,只是将 [] 换成了 {},并将单个表达式 i * i 换成了键值对表达式 key: str(key)。这种设计上的一致性,大大降低了我们的学习和记忆负担。

那么,元组(tuple)呢?如果我们顺着上面的思路,把方括号 [] 换成圆括号 (),会得到一个“元组推导式”吗?

my_tuple_gen = (i for i in range(10))

当我们执行这行代码并打印 my_tuple_gen 时,会发现得到的结果并不是一个元组,而是一个 generator object(生成器对象)。

Note

这是一个非常有意思的设计。Python 的设计者认为,推导式常常被用在大型数据集上。如果直接生成一个完整的元组,可能会瞬间占用大量内存。而生成器则采用了一种“惰性求值”的策略,它不会一次性把所有元素都计算出来,而是在你真正需要使用它(比如在 for 循环中迭代)的时候,才一个一个地把元素“生产”出来。

这种惰性求值的特性在很多场景下非常有用。当然,如果我们确实需要一个完整的元组,也非常简单,只需要在生成器表达式外面显式地调用 tuple() 构造函数即可:

my_tuple = tuple(i for i in range(10))

总的来看,Python 的推导式提供了一种高度浓缩且极富表达力的方式来创建集合、列表和字典。它不仅仅是代码量的减少,更是一种思维模式的转变——从描述“如何做”(how)的指令式循环,转向描述“是什么”(what)的声明式表达。

而这种声明式的表达,不仅仅带来了代码可读性的提升。一个经常被提及但同样重要的优点是,推导式在执行效率上也往往优于等价的 for 循环。其背后的原因在于,推导式的循环逻辑是在 C 语言层面实现的,这绕过了 Python 解释器在处理循环时的部分开销,从而获得了更好的性能。因此,熟练运用它,不仅能让我们的代码更加 Pythonic,也常常能让代码跑得更快。