Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 798 – 解构推导式

作者:
Adam Hartz <hz at mit.edu>, Erik Demaine <edemaine at mit.edu>
发起人:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
讨论至:
Discourse 帖子
状态:
草案
类型:
标准跟踪
创建日期:
2025年7月19日
Python 版本:
3.15
发布历史:
2021年10月16日, 2025年6月22日, 2025年7月19日

目录

摘要

本PEP提议扩展列表、集合和字典推导式以及生成器表达式,允许在表达式开头使用解构语法(***),从而提供一种简洁的方式将任意数量的可迭代对象组合成一个列表、集合或生成器,或者将任意数量的字典组合成一个字典,例如:

[*it for it in its]  # list with the concatenation of iterables in 'its'
{*it for it in its}  # set with the union of iterables in 'its'
{**d for d in dicts} # dict with the combination of dicts in 'dicts'
(*it for it in its)  # generator of the concatenation of iterables in 'its'

动机

来自 PEP 448 的扩展解构语法(***)使得组合少量可迭代对象或字典变得容易。

[*it1, *it2, *it3]  # list with the concatenation of three iterables
{*it1, *it2, *it3}  # set with the union of three iterables
{**dict1, **dict2, **dict3}  # dict with the combination of three dicts

但是,如果我们要以类似的方式组合任意数量的可迭代对象,我们就不能以这种方式使用解构。

尽管如此,我们确实有几种组合多个可迭代对象的方法。例如,我们可以使用显式循环结构和内置的组合方法:

new_list = []
for it in its:
    new_list.extend(it)

new_set = set()
for it in its:
    new_set.update(it)

new_dict = {}
for d in dicts:
    new_dict.update(d)

def new_generator():
    for it in its:
        yield from it

或者,我们可以通过使用带有两个循环的推导式来使其更简洁:

[x for it in its for x in it]
{x for it in its for x in it}
{key: value for d in dicts for key, value in d.items()}
(x for it in its for x in it)

或者,我们可以使用 itertools.chainitertools.chain.from_iterable

list(itertools.chain(*its))
set(itertools.chain(*its))
dict(itertools.chain(*(d.items() for d in dicts)))
itertools.chain(*its)

list(itertools.chain.from_iterable(its))
set(itertools.chain.from_iterable(its))
dict(itertools.chain.from_iterable(d.items() for d in dicts))
itertools.chain.from_iterable(its)

或者,除了生成器之外,我们可以使用 functools.reduce

functools.reduce(operator.iconcat, its, (new_list := []))
functools.reduce(operator.ior, its, (new_set := set()))
functools.reduce(operator.ior, its, (new_dict := {}))

本PEP提议允许在推导式中使用解构操作作为额外的替代方案:

[*it for it in its]  # list with the concatenation of iterables in 'its'
{*it for it in its}  # set with the union of iterables in 'its'
{**d for d in dicts} # dict with the combination of dicts in 'dicts'
(*it for it in its)  # generator of the concatenation of iterables in 'its'

此提案还扩展到异步推导式和生成器表达式,例如,(*ait async for ait in aits()) 等价于 (x async for ait in aits() for x in ait)

基本原理

将可迭代对象组合成一个更大的对象是一项常见的任务。例如,一篇询问如何将列表的列表扁平化的 StackOverflow帖子 已被浏览460万次。尽管这是一项常见的操作,但目前可用的简洁执行选项需要间接层,这可能会使生成的代码难以阅读和理解。

提议的语法简洁(避免使用和重复辅助变量),并且我们期望对于熟悉推导式和解构语法的程序员来说是直观和熟悉的(请参阅 代码示例,其中包含可以利用提议的语法更清晰简洁地重写标准库代码的示例)。

该提案部分是受到Python编程课程书面考试的启发,其中几名学生在他们的解决方案中使用了该语法(特别是 set 版本),假设它在Python中已经存在。这表明该语法是直观的,即使对于初学者也是如此。相比之下,现有语法 [x for it in its for x in it] 是学生经常犯错的一种,许多学生的自然冲动是颠倒 for 子句的顺序。

此外,本PEP发布后的一篇 Reddit帖子 的评论区显示了对该提案的大力支持,并进一步表明此处提出的语法是清晰、直观和有用的。

规范

语法

应更改语法以允许列表/集合推导式和生成器表达式中的表达式前面带有 *,并允许字典推导式的替代形式,其中可以在 key: value 对的位置使用双星号表达式。

这可以通过更新 listcompsetcomp 规则以使用 star_named_expression 代替 named_expression 来实现

listcomp[expr_ty]:
    | '[' a=star_named_expression b=for_if_clauses ']'

setcomp[expr_ty]:
    | '{' a=star_named_expression b=for_if_clauses '}'

同样,genexp 的规则也需要修改,以允许 starred_expression

genexp[expr_ty]:
    | '(' a=(assignment_expression | expression !':=' | starred_expression) b=for_if_clauses ')'

字典推导式的规则也需要调整,以允许这种新形式

dictcomp[expr_ty]:
    | '{' a=double_starred_kvpair b=for_if_clauses '}'

不应对函数调用中参数解构的处理方式进行更改,即,提供给函数的唯一参数的生成器表达式不需要额外的冗余括号的一般规则应予以保留。请注意,这意味着,例如,f(*x for x in it) 等价于 f((*x for x in it))(有关更多讨论,请参阅 星号生成器作为函数参数)。

*** 应该只允许在推导式中表达式的最顶层使用(有关更多讨论,请参阅 进一步推广解构运算符)。

语义:列表/集合/字典推导式

列表推导式 [*expr for x in it] 中星号表达式的含义是将每个表达式视为一个可迭代对象,并将它们连接起来,就像它们通过 [*expr1, *expr2, ...] 显式列出一样。类似地,{*expr for x in it} 形成一个集合并集,就像表达式通过 {*expr1, *expr2, ...} 显式列出一样;{**expr for x in it} 组合字典,就像表达式通过 {**expr1, **expr2, ...} 显式列出一样。这些操作应保留以这种方式组合集合的所有等效语义(例如,在组合字典时,重复键的情况下,后面的值替换前面的值)。

换句话说,以下推导式创建的对象

new_list = [*expr for x in its]
new_set = {*expr for x in its}
new_dict = {**expr for d in dicts}

应分别等同于以下代码段创建的对象

new_list = []
for x in its:
    new_list.extend(expr)

new_set = set()
for x in its:
    new_set.update(expr)

new_dict = {}
for x in dicts:
    new_dict.update(expr)

语义:生成器表达式

使用解构语法的生成器表达式应形成新的生成器,从表达式给定的可迭代对象的连接中产生值。具体来说,其行为被定义为等效于以下内容

# equivalent to g = (*expr for x in it)
def generator():
    for x in it:
        yield from expr

g = generator()

由于在异步生成器中不允许使用 yield from(请参阅 PEP 525 中关于异步 yield from 的部分),因此 (*expr async for x in ait()) 的等效形式更像是以下内容(尽管这种新形式当然不应定义或引用循环变量 i

# equivalent to g = (*expr async for x in ait())
async def generator():
    async for x in ait():
        for i in expr:
            yield i

g = generator()

这些语义的细节应在未来重新审视,特别是如果异步生成器获得对 yield from 的支持(在这种情况下,异步变体可能希望更改为使用 yield from 而不是显式循环)。有关更多讨论,请参阅 替代生成器表达式语义

与赋值表达式的交互

请注意,此提案不建议更改推导式各个部分的求值顺序,也不建议更改任何关于作用域的规则。这对于使用来自 PEP 500 的“海象运算符” := 的生成器表达式尤其相关,该运算符在推导式或生成器表达式中使用时,其变量绑定发生在包含作用域而不是推导式本地作用域。

举例来说,考虑求值表达式 (*(y := [i, i+1]) for i in (0, 2, 4)) 所产生的生成器。这大致等效于以下生成器,不同之处在于其生成器表达式形式中,y 将绑定到包含作用域而不是本地作用域。

def generator():
    for i in (0, 2, 4):
        yield from (y := [i, i+1])

在此示例中,子表达式 (y := [i, i+1]) 在生成器耗尽之前精确求值三次:分别在推导式中将 i 赋值为 024 之后。因此,y(在包含作用域中)将在那些时间点被修改。

>>> g = (*(y := [i, i+1]) for i in (0, 2, 4))
>>> y
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
    y
NameError: name 'y' is not defined
>>> next(g)
0
>>> y
[0, 1]
>>> next(g)
1
>>> y
[0, 1]
>>> next(g)
2
>>> y
[2, 3]

错误报告

目前,提议的语法会产生一个 SyntaxError。允许这些形式被识别为语法有效需要调整 invalid_comprehensioninvalid_dict_comprehension 的语法规则,以分别允许使用 ***

至少在以下情况下应提供额外的特定错误消息

  • 尝试在列表推导式或生成器表达式中使用 ** 应报告字典解构不能在这些结构中使用,例如:
    >>> [**x for x in y]
      File "<stdin>", line 1
        [**x for x in y]
         ^^^
    SyntaxError: cannot use dict unpacking in list comprehension
    
    >>> (**x for x in y)
      File "<stdin>", line 1
        (**x for x in y)
         ^^^
    SyntaxError: cannot use dict unpacking in generator expression
    
  • 对于尝试在字典键/值中使用 * 的现有错误消息应予以保留,但在尝试对字典键或值使用 ** 解构时,应报告类似的消息,例如:
    >>> {*k: v for k,v in items}
      File "<stdin>", line 1
        {*k: v for k,v in items}
         ^^
    SyntaxError: cannot use a starred expression in a dictionary key
    
    >>> {k: *v for k,v in items}
      File "<stdin>", line 1
        {k: *v for k,v in items}
            ^^
    SyntaxError: cannot use a starred expression in a dictionary value
    
    >>> {**k: v for k,v in items}
      File "<stdin>", line 1
        {**k: v for k,v in items}
         ^^^
    SyntaxError: cannot use dict unpacking in a dictionary key
    
    >>> {k: **v for k,v in items}
      File "<stdin>", line 1
        {k: **v for k,v in items}
            ^^^
    SyntaxError: cannot use dict unpacking in a dictionary value
    
  • 其他一些现有错误消息的措辞也应进行类似调整,以考虑新语法的存在,和/或更普遍地澄清与解构相关的模糊或令人困惑的情况(特别是 进一步推广解构运算符 中提及的情况),例如:
    >>> [*x if x else y]
      File "<stdin>", line 1
        [*x if x else y]
         ^^^^^^^^^^^^^^
    SyntaxError: invalid starred expression. Did you forget to wrap the conditional expression in parentheses?
    
     >>> {**x if x else y}
      File "<stdin>", line 1
        {**x if x else y}
         ^^^^^^^^^^^^^^^
    SyntaxError: invalid double starred expression. Did you forget to wrap the conditional expression in parentheses?
    
    >>> [x if x else *y]
      File "<stdin>", line 1
        [x if x else *y]
                     ^
    SyntaxError: cannot unpack only part of a conditional expression
    
    >>> {x if x else **y}
      File "<stdin>", line 1
        {x if x else **y}
                     ^^
    SyntaxError: cannot use dict unpacking on only part of a conditional expression
    

参考实现

参考实现 实现了此功能,包括草案文档和附加测试用例。

向后兼容性

目前所有语法有效的推导式的行为将不受此更改的影响,因此我们预计不会出现太多向后不兼容的问题。原则上,此更改只会影响依赖于在推导式中尝试使用解构操作会引发 SyntaxError 的代码,或依赖于被替换的旧错误消息的特定措辞的代码,我们预计这种情况很少见。

一个相关的担忧是,如果未来做出假设性决定,改变异步生成器表达式的语义,使其在解构过程中使用 yield from(委托给正在解构的生成器),这将是不向后兼容的,因为它会影响在使用 .asend().athrow().aclose() 时生成器的行为。尽管如此,尽管不向后兼容,但这种更改不太可能产生很大的影响,因为它只会影响在此提案下不特别有用的结构的行为。有关更多讨论,请参阅 替代生成器表达式语义

代码示例

本节展示了一些说明性示例,说明如何重写标准库中的小段代码以利用这种新语法来提高简洁性和可读性。参考实现 在进行这些替换后仍然通过了所有测试。

替换显式循环

替换显式循环将多行代码压缩为一行,并避免了定义和引用辅助变量的需要。

  • 来自 email/_header_value_parser.py
    # current:
    comments = []
    for token in self:
        comments.extend(token.comments)
    return comments
    
    # improved:
    return [*token.comments for token in self]
    
  • 来自 shutil.py
    # current:
    ignored_names = []
    for pattern in patterns:
        ignored_names.extend(fnmatch.filter(names, pattern))
    return set(ignored_names)
    
    # improved:
    return {*fnmatch.filter(names, pattern) for pattern in patterns}
    
  • 来自 http/cookiejar.py
    # current:
    cookies = []
    for domain in self._cookies.keys():
        cookies.extend(self._cookies_for_domain(domain, request))
    return cookies
    
    # improved:
    return [
        *self._cookies_for_domain(domain, request)
        for domain in self._cookies.keys()
    ]
    

替换 from_iterable 及其变体

虽然并非总是正确的选择,但替换 itertools.chain.from_iterablemap 可以避免额外的间接层,从而使代码遵循惯例,即推导式比 map/filter 更具可读性。

  • 来自 dataclasses.py
    # current:
    inherited_slots = set(
        itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
    )
    
    # improved:
    inherited_slots = {*_get_slots(c) for c in cls.__mro__[1:-1]}
    
  • 来自 importlib/metadata/__init__.py
    # current:
    return itertools.chain.from_iterable(
        path.search(prepared) for path in map(FastPath, paths)
    )
    
    # improved:
    return (*FastPath(path).search(prepared) for path in paths)
    
  • 来自 collections/__init__.py (Counter 类)
    # current:
    return _chain.from_iterable(_starmap(_repeat, self.items()))
    
    # improved:
    return (*_repeat(elt, num) for elt, num in self.items())
    
  • 来自 zipfile/_path/__init__.py
    # current:
    parents = itertools.chain.from_iterable(map(_parents, names))
    
    # improved:
    parents = (*_parents(name) for name in names)
    
  • 来自 _pyrepl/_module_completer.py
    # current:
    search_locations = set(chain.from_iterable(
        getattr(spec, 'submodule_search_locations', [])
        for spec in specs if spec
    ))
    
    # improved:
    search_locations = {
        *getattr(spec, 'submodule_search_locations', [])
        for spec in specs if spec
    }
    

替换推导式中的双重循环

替换推导式中的双重循环避免了定义和引用辅助变量的需要,减少了混乱。

  • 来自 importlib/resources/readers.py
    # current:
    children = (child for path in self._paths for child in path.iterdir())
    
    # improved:
    children = (*path.iterdir() for path in self._paths)
    
  • 来自 asyncio/base_events.py
    # current:
    exceptions = [exc for sub in exceptions for exc in sub]
    
    # improved:
    exceptions = [*sub for sub in exceptions]
    
  • 来自 _weakrefset.py
    # current:
    return self.__class__(e for s in (self, other) for e in s)
    
    # improved:
    return self.__class__(*s for s in (self, other))
    

如何教授此内容

目前,介绍推导式概念的常见方法(Python教程采用的方法)是演示等效代码。例如,这种方法会说,out = [expr for x in it] 等效于以下代码:

out = []
for x in it:
    out.append(expr)

采用这种方法,我们可以将 out = [*expr for x in it] 引入为等效于以下代码(它使用 extend 而不是 append):

out = []
for x in it:
    out.extend(expr)

利用解构的集合和字典推导式也可以通过类似的类比来介绍

# equivalent to out = {expr for x in it}
out = set()
for x in it:
    out.add(expr)

# equivalent to out = {*expr for x in it}
out = set()
for x in it:
    out.update(expr)

# equivalent to out = {k_expr: v_expr for x in it}
out = {}
for x in it:
    out[k_expr] = v_expr

# equivalent to out = {**expr for x in it}, provided that expr evaluates to
# a mapping that can be unpacked with **
out = {}
for x in it:
    out.update(expr)

我们可以采用类似的方法来说明涉及解构的生成器表达式的行为

# equivalent to g = (expr for x in it)
def generator():
    for x in it:
        yield expr
g = generator()

# equivalent to g = (*expr for x in it)
def generator():
    for x in it:
        yield from expr
g = generator()

然后我们可以从这些具体示例中推广到这样的想法:无论何时,非星号推导式/生成器表达式会使用添加单个元素到集合的操作符,而星号则会使用添加多个元素到该集合的操作符。

或者,我们不需要将这两个想法分开;相反,有了新语法,我们可以认为 out = [...x... for x in it] 等同于以下内容 [1] (其中 ...x... 是任意代码的占位符),无论 ...x... 是否使用 *

out = []
for x in it:
    out.extend([...x...])

同样,我们可以认为 out = {...x... for x in it} 等同于以下代码,无论 ...x... 是否使用 ***:

out = set()  # or out = {}
for x in it:
    out.update({...x...})

这些示例在它们产生的输出在带有推导式和不带有推导式的版本中都是相同的意义上是等价的,但请注意,非推导式版本由于在每次 extendupdate 之前都创建新的列表/集合/字典而效率略低,这在使用推导式的版本中是不必要的。

被拒绝的替代提案

在思考上述规范时,主要目标是与围绕解构和推导式/生成器表达式的现有规范保持一致。一种解释方式是,目标是编写规范,以便对现有语法和代码生成所需的更改尽可能小,让现有代码指导周围的语义。

下面我们讨论在讨论中提出但未包含在本提案中的一些常见担忧/替代提案。

星号生成器作为函数参数

一个多次出现的常见问题(不仅在上面链接的讨论帖中,而且在之前关于这个相同想法的讨论中)是当将星号生成器作为唯一参数传递给 f(*x for x in y) 时可能存在的语法歧义。在最初的 PEP 448 中,这种歧义被引用为不将类似泛化纳入提案的原因。

本提案建议将 f(*x for x in y) 解释为 f((*x for x in y)),不应尝试进一步解构生成的生成器,但我们的讨论中(和/或过去)提出了几种替代方案,包括:

  • f(*x for x in y) 解释为 f(*(x for x in y)
  • f(*x for x in y) 解释为 f(*(*x for x in y)),或者
  • 即使本提案的其他方面被接受,对于 f(*x for x in y) 仍继续引发 SyntaxError

之所以更倾向于本提案而不是这些替代方案,是为了保留现有关于生成器表达式标点符号的约定。目前,一般规则是生成器表达式必须用括号括起来,除非作为函数的唯一参数提供,而本提案建议即使我们允许更多种类的生成器表达式,也要保持该规则。此选项保持了使用解构和不使用解构的推导式和生成器表达式之间的完全对称。

目前,我们有以下约定:

f([x for x in y])  # pass in a single list
f({x for x in y})  # pass in a single set
f(x for x in y)  # pass in a single generator (no additional parentheses required around genexp)

f(*[x for x in y])  # pass in elements from the list separately
f(*{x for x in y})  # pass in elements from the set separately
f(*(x for x in y))  # pass in elements from the generator separately (parentheses required)

本提案选择在推导式使用解构时也保持这些约定

f([*x for x in y])  # pass in a single list
f({*x for x in y})  # pass in a single set
f(*x for x in y)  # pass in a single generator (no additional parentheses required around genexp)

f(*[*x for x in y])  # pass in elements from the list separately
f(*{*x for x in y})  # pass in elements from the set separately
f(*(*x for x in y))  # pass in elements from the generator separately (parentheses required)

进一步推广解构运算符

讨论中提出的另一个建议涉及进一步推广 *,而不仅仅是允许它在推导式中用于解构表达式。此扩展主要考虑了两种形式:

  • *** 变成真正的单目运算符,创建一种新的 Unpackable 对象(或类似对象),推导式可以通过解构来处理它,但也可以在其他上下文中使用;或者
  • 继续只允许在表达式列表、推导式、生成器表达式和参数列表等本提案中允许的地方使用 ***,但也允许它们在推导式中的子表达式中使用,例如,允许以下方式来展平包含一些可迭代对象但一些不可迭代对象的列表
    [*x if isinstance(x, Iterable) else x for x in [[1,2,3], 4]]
    

这些变体被认为过于复杂(无论是理解还是实现),且实用性不大,因此本PEP中未包含。因此,这些形式应继续引发 SyntaxError,但带有如上所述的新错误消息,尽管不应排除将其作为未来提案的考虑。

替代生成器表达式语义

另一个讨论焦点围绕着生成器表达式中解构的语义,特别是考虑到异步生成器不支持 yield from 的情况下,同步和异步生成器表达式语义之间的关系(参见 PEP 525 中关于异步 yield from 的部分)。

核心问题是同步和异步生成器表达式在解构时是否应该使用 yield from(或等效),而不是显式循环。这些选项之间的主要区别在于生成的生成器是否委托给正在解构的对象,这将影响这些生成器表达式在使用 .send()/.asend().throw()/.athrow().close()/.aclose() 时的行为,如果正在解构的对象本身是生成器。这些选项之间的差异总结在 附录:生成器委托的语义 中。

考虑了几种合理的选项,其中没有一个在 Discourse 帖子中的投票 中明确胜出。除了上面概述的提案外,还考虑了以下选项:

  1. 同步和异步生成器表达式都使用显式循环。

    这种策略将导致同步和异步生成器表达式之间对称,但会阻止通过不允许在同步生成器表达式中进行委托而可能获得有用工具。这种方法的一个具体担忧是引入同步和异步生成器之间的不对称,但这种担忧因同步和异步生成器之间普遍存在这些不对称而得到缓解。

  2. 在同步生成器表达式中使用 yield from 进行解构,并在异步生成器表达式中模仿 yield from 的行为进行解构。

    这种策略也会使同步和异步生成器中的解构行为对称,但会更复杂,以至于成本可能不值得收益。因此,本PEP提议,使用解构运算符的生成器表达式不应使用类似于 yield from 的语义,直到异步生成器普遍支持 yield from

  3. 在同步生成器表达式中使用 yield from 进行解包,并且在异步生成器表达式支持 yield from 之前禁止在其中进行解包。

    如果异步生成器表达式未来获得对 yield from 的支持,这种策略可能会减少摩擦,确保届时做出的任何决定都将完全向后兼容。但是,在这种上下文中使用解构的实用性似乎超过了未来在异步生成器表达式获得对 yield from 的支持时可能出现的最小入侵性向后不兼容更改的潜在弊端。

  4. 禁止在所有生成器表达式中进行解构。

    这将保持两种情况之间的对称性,但缺点是会失去一种非常有表现力的形式。

这些选项(包括本PEP中提出的选项)各有优缺点,没有一个选项在所有方面都明显优越。语义:生成器表达式 中提出的语义代表了一种合理的折衷方案,即同步和异步生成器表达式中的解构反映了目前编写等效生成器的常见方式。此外,这些细微差异不太可能对常见用例产生影响(例如,对于最常见的组合简单集合的用例,没有区别)。

如上所述,如果异步生成器将来获得对 yield from 的支持,则应重新审视此决定,在这种情况下,应考虑调整异步生成器表达式中解构的语义以使用 yield from

担忧与缺点

尽管讨论帖中的普遍共识似乎是这种语法清晰直观,但也提出了一些担忧和潜在缺点。本节旨在总结这些担忧。

  • 与现有替代方案重叠: 尽管提议的语法可以说更清晰简洁,但Python中已经有几种方法可以实现相同的功能。
  • 函数调用歧义: 诸如 f(*x for x in y) 之类的表达式最初可能看起来模棱两可,因为不清楚意图是解构生成器还是将其作为单个参数传递。尽管此提案通过将该形式视为等同于 f((*x for x in y)) 来保留现有约定,但这种等价关系可能不那么显而易见。
  • 过度使用或滥用的可能性: 推导式中复杂的解构用法可能会掩盖在显式循环中会更清晰的逻辑。虽然这在更一般的推导式中已经是一个问题,但 *** 的添加可能会使特别复杂的用法更难以一眼阅读和理解。例如,尽管这些情况可能很少见,但以多种方式使用解构的推导式可能会使人难以知道正在解构什么以及何时解构:f(*(*x for *x, _ in list_of_lists))
  • 作用域限制不明确: 本提案将解构限制在推导式表达式的顶层,但一些用户可能期望解构运算符被进一步泛化,如 进一步推广解构运算符 中所讨论。
  • 对外部工具的影响: 与Python语法的任何更改一样,进行此更改将给代码格式化程序、linter、类型检查器等维护者带来工作,以确保支持新语法。

附录:其他语言

相当多的其他语言都支持这种扁平化,其语法与Python中已有的语法相似,但在推导式中使用解构语法的支持却很少。本节简要总结了几种其他语言中类似语法的支持情况。

许多支持推导式的语言都支持双重循环

# python
[x for xs in [[1,2,3], [], [4,5]] for x in xs * 2]
-- haskell
[x | xs <- [[1,2,3], [], [4,5]], x <- xs ++ xs]
# julia
[x for xs in [[1,2,3], [], [4,5]] for x in [xs; xs]]
; clojure
(for [xs [[1 2 3] [] [4 5]] x (concat xs xs)] x)

其他几种语言(即使是那些没有推导式的语言)也通过内置函数或方法支持这些操作,以支持嵌套结构的扁平化。

# python
list(itertools.chain(*(xs*2 for xs in [[1,2,3], [], [4,5]])))
// javascript
[[1,2,3], [], [4,5]].flatMap(xs => [...xs, ...xs])
-- haskell
concat (map (\x -> x ++ x) [[1,2,3], [], [4,5]])
# ruby
[[1, 2, 3], [], [4, 5]].flat_map {|e| e * 2}

然而,同时支持推导式和解构的语言往往不允许在推导式中进行解构。例如,Julia 中的以下表达式目前会导致语法错误:

[xs... for xs in [[1,2,3], [], [4,5]]]

作为一个反例,Civet 最近增加了对类似语法的支持。例如,以下是Civet中一个有效的推导式,它使用了JavaScript的 ... 语法进行解构。

for xs of [[1,2,3], [], [4,5]] then ...(xs++xs)

附录:生成器委托的语义

关于上述语义的常见问题之一是生成器表达式内部解构时使用 yield from 与使用显式循环之间的区别。由于这是生成器的一个相当高级的功能,本附录试图总结使用 yield from 的生成器和使用显式循环的生成器之间的一些关键区别。

基本行为

对于值的简单迭代,我们预计这将是生成器表达式中最常见的解构用法,两种方法都产生相同的结果

def yield_from(iterables):
    for iterable in iterables:
        yield from iterable

def explicit_loop(iterables):
    for iterable in iterables:
        for item in iterable:
            yield item

# Both produce the same sequence of values
x = list(yield_from([[1, 2], [3, 4]]))
y = list(explicit_loop([[1, 2], [3, 4]]))
print(x == y)  # prints True

高级生成器协议差异

当使用高级生成器协议方法 .send().throw().close(),并且子可迭代对象本身是生成器而不是简单序列时,差异就会显现出来。在这些情况下,使用 yield from 的版本会导致相关信号到达子生成器,而使用显式循环的版本则不会。

使用 .send() 进行委托

def sub_generator():
    x = yield "first"
    yield f"received: {x}"
    yield "last"

def yield_from():
    yield from sub_generator()

def explicit_loop():
    for item in sub_generator():
        yield item

# With yield from, values are passed through to sub-generator
gen1 = yield_from()
print(next(gen1))  # prints "first"
print(gen1.send("hello"))  # prints "received: hello"
print(next(gen1))  # prints "last"

# With explicit loop, .send() affects the outer generator; values don't reach the sub-generator
gen2 = explicit_loop()
print(next(gen2))  # prints "first"
print(gen2.send("hello"))  # prints "received: None" (sub-generator receives None instead of "hello")
print(next(gen2))  # prints "last"

使用 .throw() 进行异常处理

def sub_generator_with_exception_handling():
    try:
        yield "first"
        yield "second"
    except ValueError as e:
        yield f"caught: {e}"

def yield_from():
    yield from sub_generator_with_exception_handling()

def explicit_loop():
    for item in sub_generator_with_exception_handling():
        yield item

# With yield from, exceptions are passed to sub-generator
gen1 = yield_from()
print(next(gen1))  # prints "first"
print(gen1.throw(ValueError("test")))  # prints "caught: test"

# With explicit loop, exceptions affect the outer generator only
gen2 = explicit_loop()
print(next(gen2))  # prints "first"
print(gen2.throw(ValueError("test")))  # ValueError is raised; sub-generator doesn't see it

使用 .close() 进行生成器清理

# hold references to sub-generators so GC doesn't close the explicit loop version
references = []

def sub_generator_with_cleanup():
    try:
        yield "first"
        yield "second"
    finally:
        print("sub-generator received GeneratorExit")

def yield_from():
    try:
        g = sub_generator_with_cleanup()
        references.append(g)
        yield from g
    finally:
        print("outer generator received GeneratorExit")

def explicit_loop():
    try:
        g = sub_generator_with_cleanup()
        references.append(g)
        for item in g:
            yield item
    finally:
        print("outer generator received GeneratorExit")

# With yield from, GeneratorExit is passed through to sub-generator
gen1 = yield_from()
print(next(gen1))  # prints "first"
gen1.close()  # closes sub-generator and then outer generator

# With explicit loop, GeneratorExit goes to outer generator only
gen2 = explicit_loop()
print(next(gen2))  # prints "first"
gen2.close()  # only closes outer generator

print('program finished; GC will close the explicit loop subgenerator')
# second inner generator closes when GC closes it at the end

参考资料


来源:https://github.com/python/peps/blob/main/peps/pep-0798.rst

最后修改:2025-09-15 17:40:01 GMT