PEP 448 – 额外解包泛化
- 作者:
- Joshua Landau <joshua at landau.ws>
- 讨论至:
- Python-Ideas 邮件列表
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2013年6月29日
- Python 版本:
- 3.5
- 发布历史:
摘要
本 PEP 提出了扩展 * 可迭代解包运算符和 ** 字典解包运算符的用法,以允许在更多位置、任意次数以及在函数调用和显示中进行解包。
建议函数调用支持任意数量的解包,而不仅仅是一个
>>> print(*[1], *[2], 3)
1 2 3
>>> dict(**{'x': 1}, y=2, **{'z': 3})
{'x': 1, 'y': 2, 'z': 3}
建议在元组、列表、集合和字典显示中允许解包
>>> *range(4), 4
(0, 1, 2, 3, 4)
>>> [*range(4), 4]
[0, 1, 2, 3, 4]
>>> {*range(4), 4}
{0, 1, 2, 3, 4}
>>> {'x': 1, **{'y': 2}}
{'x': 1, 'y': 2}
在字典中,后面的值将始终覆盖前面的值
>>> {'x': 1, **{'x': 2}}
{'x': 2}
>>> {**{'x': 2}, 'x': 1}
{'x': 1}
本 PEP 不包括在列表、集合和字典推导式中使用的解包运算符,尽管这并未排除在未来的提案中。
基本原理
目前 * 可迭代解包运算符的用法具有不必要的限制,可能会损害可读性。
多次解包有明显的理由。当您想将多个可迭代对象解包到函数定义中,或者在一个解包后面跟着更多的位置参数时,最自然的方法是这样写:
function(**kw_arguments, **more_arguments)
function(*arguments, argument)
这方面有用的简单示例是 print 和 str.format。相反,您可能被迫这样写:
kwargs = dict(kw_arguments)
kwargs.update(more_arguments)
function(**kwargs)
args = list(arguments)
args.append(arg)
function(*args)
或者,如果您知道这样做:
from collections import ChainMap
function(**ChainMap(more_arguments, arguments))
from itertools import chain
function(*chain(args, [arg]))
这增加了不必要的代码噪音,而且对于第一种方法,会导致重复的工作。
在容器内部进行解包有两个主要理由。首先是赋值的对称性,其中 fst, *other, lst = elems 和 elems = fst, *other, lst 是近似的逆运算,忽略了类型的具体细节。这实际上通过消除特殊情况来简化了语言。
其次,它极大地简化了“加法”类型,例如合并字典,并且以一种明确且定义良好的方式进行:
combination = {**first_dictionary, "x": 1, "y": 2}
而不是:
combination = first_dictionary.copy()
combination.update({"x": 1, "y": 2})
这在更倾向于表达式的上下文中尤其重要。这对于将可迭代对象求和到一个列表中也是一种更具可读性的方式,例如 my_list + list(my_tuple) + list(my_range),现在它等同于 [*my_list, *my_tuple, *my_range]。
规范
函数调用可以接受无限数量的 * 和 ** 解包。对于位置参数相对于 * 解包的顺序将没有限制,对于关键字参数相对于 ** 解包的顺序也没有限制。
函数调用仍然有以下限制:关键字参数必须跟在位置参数之后,并且 ** 解包必须额外跟在 * 解包之后。
目前,如果一个参数被多次给出——例如一个位置参数既通过位置又通过关键字给出——会引发 TypeError。对于通过多个 ** 解包提供的重复参数,例如 f(**{'x': 2}, **{'x': 3}),这仍然成立,只是错误将在运行时检测到。
函数看起来像这样:
function(
argument or *args, argument or *args, ...,
kwargument or *args, kwargument or *args, ...,
kwargument or **kwargs, kwargument or **kwargs, ...
)
元组、列表、集合和字典将允许解包。这将表现为未解包项中的元素按照解包位置的顺序插入,就像在函数调用中解包一样。字典需要 ** 解包;所有其他容器需要 * 解包。
字典中的键保持从右到左的优先级顺序,因此 {**{'a': 1}, 'a': 2, **{'a': 3}} 评估为 {'a': 3}。解包的数量或位置没有限制。
缺点
函数调用中允许的参数顺序比以前更复杂。规则的最简单解释可能是“位置参数优先于关键字参数和 ** 解包;* 解包优先于 ** 解包”。
虽然 *elements, = iterable 会使 elements 成为一个列表,但 elements = *iterable, 会使 elements 成为一个元组。对于不熟悉此结构的人来说,原因可能会感到困惑。
有人担心字典中允许重复键而函数调用语法中重复键会引发错误之间的意外差异。尽管这在当前语法中已经存在,但此提案可能会加剧这个问题。这在实践中会带来多大问题,还有待观察。
变体
本 PEP 最初考虑了函数调用中参数类型(位置、关键字、* 或 **)的排序是否可以变得不那么严格。这没有得到多少支持,因此这个想法被搁置了。
本 PEP 的早期版本允许在列表、集合和字典推导式中使用解包运算符作为容器可迭代对象上的展平运算符:
>>> ranges = [range(i) for i in range(5)]
>>> [*item for item in ranges]
[0, 0, 1, 0, 1, 2, 0, 1, 2, 3]
>>> {*item for item in ranges}
{0, 1, 2, 3}
这受到了关于可读性的强烈担忧和温和支持的混合反馈。为了不影响 PEP 中争议较小的方面,这没有与提案的其余部分一起被接受。
函数调用中不带括号的推导式,例如 f(x for x in it),已经有效。这些可以扩展为:
f(*x for x in it) == f((*x for x in it))
f(**x for x in it) == f({**x for x in it})
然而,尚不清楚这是否是最佳行为,或者它是否应该解包到对 f 的调用的参数中。由于这可能会令人困惑且实用性极低,因此本 PEP 中不包含它。相反,这些将抛出 SyntaxError,并且应该改用带有显式括号的推导式。
批准
本 PEP 于 2015 年 2 月 25 日被 Guido 接受 [1]。
实施
Python 3.5 的实现在 bug 跟踪器上的 Issue 2292 中找到 [2]。这目前包括对推导式内部解包的支持,这应该被移除。
参考资料
[3] Python-ideas 列表上的讨论,“list / array comprehensions extension”,Alexander Heger (https://mail.python.org/pipermail/python-ideas/2011-December/013097.html)
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0448.rst