0%

Python 3.15 新特性:在推导式内部解包

转载声明:本文翻译转载自 mathspp.com 的文章 Unpacking inside comprehensions,作者 Rodrigo Girão Serrão,原文发布于 2026 年 10 月 15 日。本文为翻译转载,版权归原作者所有。

从 Python 3.15 开始,你可以在列表推导式内部用星号 * 进行解包:

1
2
3
nested = [[0, 1, 2], [3, 4], [], [5, 6, 7]]
flattened = [*sublist for sublist in nested]
print(flattened) # [0, 1, 2, 3, 4, 5, 6, 7]

这个新的解包特性同样适用于集合推导式、字典推导式和生成器表达式。本文将讲解这套语法、Python 3.14 及更早版本中的替代写法,并展示一些真实世界的应用示例。

「Python 3.15 的 15 天」系列

为了庆祝 Python 3.15 发布,作者将在接下来的 15 个工作日里,每天讲解一个 Python 3.15 的新特性——解释清晰、附带示例,让你不必自己去翻变更日志。

在推导式内部解包

如果你有一个嵌套的列表的列表,可以通过在推导式内部解包把它拍平:

1
2
3
nested = [[0, 1, 2], [3, 4], [], [5, 6, 7]]
flattened = [*sublist for sublist in nested]
print(flattened) # [0, 1, 2, 3, 4, 5, 6, 7]

在 3.15 引入解包语法之前,你得写一个嵌套的列表推导式:

1
2
3
4
5
6
7
nested = [[0, 1, 2], [3, 4], [], [5, 6, 7]]
flattened = [
element
for sublist in nested
for element in sublist
]
print(flattened) # [0, 1, 2, 3, 4, 5, 6, 7]

嵌套列表推导式的结构,对应着等效的嵌套 for 循环的结构:

1
2
3
4
5
nested = [[0, 1, 2], [3, 4], [], [5, 6, 7]]
for sublist in nested:
for element in sublist:
flattened.append(element)
print(flattened) # [0, 1, 2, 3, 4, 5, 6, 7]

如果你熟悉列表方法,就会知道去掉内层循环、改用 extend 方法,可以把嵌套循环的版本写得更地道:

1
2
3
4
nested = [[0, 1, 2], [3, 4], [], [5, 6, 7]]
for sublist in nested:
flattened.extend(sublist)
print(flattened) # [0, 1, 2, 3, 4, 5, 6, 7]

调用 extend 可以把 sublist 的所有元素加进拍平后的列表,而无需显式迭代——这正和在推导式内部解包如出一辙。事实上,「在 for 循环里调用 extend」正是你应该改用「在列表推导式内部解包」的信号。

在集合推导式内部解包

在集合推导式内部解包的方式完全相同,只是最终的集合类型是集合(set)而非列表:

1
2
3
nested = [[0, 1, 2], [3, 4], [], [5, 6, 7]]
flattened = {*sublist for sublist in nested}
print(flattened) # {0, 1, 2, 3, 4, 5, 6, 7}

上面的代码与前面的例子一致,但创建的是一个扁平的集合而不是列表。

再举一个例子,下面的代码利用集合的 update 方法,从一个字符串列表中收集所有去重后的字符:

1
2
3
4
5
6
7
words = ["hello", "world"]

characters = set()
for word in words:
characters.update(word)

print(characters) # {'e', 'r', 'h', 'o', 'l', 'w', 'd'}

在 for 循环里用 set.update,类似于在 for 循环里用 list.extend——它也是一个信号,说明你可能想改用「在集合推导式内部解包」:

1
2
3
words = ["hello", "world"]
characters = {*word for word in words}
print(characters) # {'e', 'r', 'h', 'o', 'l', 'w', 'd'}

在字典推导式内部解包

字典同样可以在字典推导式内部解包,但你需要用两个星号 ** 而不是一个 *:

1
2
3
4
5
6
configs = [
{"user": "<default>", "font_size": 18},
{"user": "rodrigo"},
]
config = {**conf for conf in configs}
print(config) # {'user': 'rodrigo', 'font_size': 18}

在字典推导式内部解包字典,优于在 for 循环里反复调用 dict.update:

1
2
3
4
5
6
7
8
9
10
configs = [
{"user": "<default>", "font_size": 18},
{"user": "rodrigo"},
]

config = {}
for conf in configs:
config.update(conf)

print(config) # {'user': 'rodrigo', 'font_size': 18}

代码示例

本节将展示几个标准库中的例子,它们都可以改用「在推导式内部解包」。

shutil 中忽略名单的构建

shutil 模块中有一个函数,从一系列模式(pattern)生成一个忽略名单列表:

1
2
3
4
5
# shutil.py
ignored_names = []
for pattern in patterns:
ignored_names.extend(fnmatch.filter(names, pattern))
return set(ignored_names)

这段代码正是「在 for 循环里调用 list.extend」的典型模式。也就是说,它可以改写为:

1
2
3
4
return {
*fnmatch.filter(names, pattern)
for pattern in patterns
}

dataclasses 中 slots 的计算

dataclasses 模块中有一段代码,用于计算 dataclass 将会继承的 slots:

1
2
3
inherited_slots = set(
itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
)

这里用 itertools.chain.from_iterable 来拍平 map(_get_slots, cls.__mro__[1:-1]) 产出的「slots 的可迭代对象」,然后再包一层 set,构建出所有被继承 slots 的集合。

为了帮助理解上面的代码,下面演示如何用 chain.from_iterable 拍平一个嵌套的可迭代对象:

1
2
3
4
5
from itertools import chain

nested = [[0, 1, 2], [3, 4], [], [5, 6, 7]]
flattened = list(chain.from_iterable(nested))
print(flattened) # [0, 1, 2, 3, 4, 5, 6, 7]

使用 chain.from_iterable 的优势在于它是惰性的——如果你需要迭代嵌套可迭代对象的元素,chain.from_iterable 是最佳选择。然而,在像 dataclasses 这样的场景里,对 chain.from_iterable 的调用只是个中间步骤,因为你拿到链之后马上就转成了集合:

1
2
3
inherited_slots = set(
itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
)

chain.from_iterable 在这里只是个辅助手段,用于得到一个包含 cls 方法解析顺序(__mro__)中所有类的全部 slots 的集合。既然这里根本没利用到 chain.from_iterable 的惰性,就可以换成「在集合推导式内部解包」:

1
inherited_slots = {*_get_slots(c) for c in cls.__mro__[1:-1]}

改写后的代码更优雅,因为它更直接。

在生成器表达式内部解包

生成器表达式同样支持在内部解包。一般来说,生成器表达式

1
g = (*sub for sub in iterable)

等效于一个带双层 for 循环的生成器函数:

1
2
3
4
5
6
def generator(iterable):
for sub in iterable:
for value in sub:
yield value

g = generator()

再举一例:在 Python 3.14 及更早版本中,chain.from_iterable 是惰性拍平可迭代对象最方便的方式。现在,chain.from_iterable 可以被替换为带解包的生成器表达式:

1
2
3
4
5
6
# Before:
from itertools import chain
chain.from_iterable(map(_get_slots, cls.__mro__[1:-1])) # From `dataclasses.py`

# After:
(*_get_slots(c) for c in cls.__mro__[1:-1])

上面的代码片段基于 dataclasses 的那段代码给出了具体示例,只是去掉了最后构建集合这一步。一般而言,调用

1
chain.from_iterable(iterable)

可以变成

1
(*sub for sub in iterable)

当可迭代对象是一个更复杂的表达式时——比如前例中的 map(...)——使用生成器表达式尤其有用。

Python 3.15 的精彩远不止于此

从两个新内置的 frozendict 和 sentinel,到惰性导入(lazy imports)、包启动配置文件、typing 改进,以及更多色彩支持,Python 3.15 还有很多值得一看的东西。如果你想进一步了解 Python 3.15 的新特性,可以看看作者正在连载的「Python 3.15 的 15 天」系列——15 天里每天讲解一个 Python 3.15 新特性。

参考资料

扫码加入技术交流群🖱️
QR code