PEP 472 – 支持带关键字参数的索引
- 作者:
- Stefano Borini, Joseph Martinot-Lagarde
- 讨论至:
- Python-Ideas 邮件列表
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建日期:
- 2014年6月24日
- Python 版本:
- 3.6
- 发布历史:
- 2014年7月2日
- 决议:
- Python-Dev 消息
摘要
本 PEP 提议扩展索引操作以支持关键字参数。形式为 a[K=3,R=2] 的表示法将成为合法的语法。出于未来兼容性考虑,a[1:2, K=3, R=4] 也被考虑在内,并且可能被允许,具体取决于实现的选择。除了对解析器的更改外,索引协议(__getitem__、__setitem__ 和 __delitem__)也可能需要调整。
动机
索引语法带有很强的语义内容,这使其区别于方法调用:它暗含了对数据子集的引用。我们认为这种语义关联很重要,并希望扩展用于引用该数据的策略。
总的来说,索引操作所需的索引数量取决于数据的维度:一维数据(例如列表)需要一个索引(例如 a[3]),二维数据(例如矩阵)需要两个索引(例如 a[2,3]),依此类推。每个索引都是沿着维度轴的选择器,索引元组中的位置是关联每个索引到相应轴的元信息。
当前的 Python 语法完全侧重于通过位置来表达与轴的关联,并且还包含用于引用非点状选择(切片)的语法糖。
>>> a[3] # returns the fourth element of a
>>> a[1:10:2] # slice notation (extract a non-trivial data subset)
>>> a[3,2] # multiple indexes (for multidimensional arrays)
本 PEP 中提出的附加表示法将允许在索引操作中使用涉及关键字参数的表示法,例如:
>>> a[K=3, R=2]
这将允许通过约定俗成的名称来引用轴。
此外,还必须考虑允许同时使用位置和关键字指定的扩展形式:
>>> a[3,R=3,K=4]
本 PEP 将探讨不同的策略来实现这些表示法的使用。
用例
以下实际用例呈现了关键字规范使用的两大类:索引和上下文选项。对于索引:
- 为索引提供更具沟通意义的解释,例如防止索引被意外颠倒。
>>> gridValues[x=3, y=5, z=8] >>> rain[time=0:12, location=location]
- 在某些领域,例如计算物理和化学,使用
Basis[Z=5]这样的表示法是为了代表精度级别的一种领域特定语言表示法。>>> low_accuracy_energy = computeEnergy(molecule, BasisSet[Z=3])
在这种情况下,索引操作将返回一个具有所选精度级别的基集(由参数 Z 表示)。索引背后的原因是 BasisSet 对象可以内部表示为数字表,其中行(在本例中对用户隐藏的“系数”轴)与单个元素相关联(例如,第 0 行:5 包含元素 1 的系数,第 5 行:8 包含元素 2 的系数),而每列与给定的精度度量(“精度”或“Z”轴)相关联,因此第一列是低精度,第二列是中等精度,依此类推。通过此索引,用户将获得另一个表示精度级别 3 的内部表的列内容的对象的对象。
此外,关键字规范可用作索引的上下文选项。具体来说:
- “默认”选项允许指定索引不存在时返回的默认值。
>>> lst = [1, 2, 3] >>> value = lst[5, default=0] # value is 0
- 对于稀疏数据集,指定插值策略以从例如周围数据推断缺失点。
>>> value = array[1, 3, interpolate=spline_interpolator]
- 可以使用相同的机制指定单位。
>>> value = array[1, 3, unit="degrees"]
如何解释表示法取决于实现类。
当前实现
目前,索引操作由方法 __getitem__、__setitem__ 和 __delitem__ 处理。这些方法的签名接受一个索引参数(__setitem__ 接受一个额外的设置为值参数)。在下文中,我们将仅分析 __getitem__(self, idx),并隐含了对其他两个方法的相同考虑。
执行索引操作时,会调用 __getitem__(self, idx)。传统上,方括号内的全部内容会被转换为一个对象传递给参数 idx。
- 当传递单个元素时,例如
a[2],idx将是2。 - 当传递多个元素时,它们必须用逗号分隔:
a[2, 3]。在这种情况下,idx将是一个元组(2, 3)。对于a[2, 3, "hello", {}]),idx将是(2, 3, "hello", {})。 - 切片表示法,例如
a[2:10],将生成一个切片对象,或者如果传递了多个值,则是一个包含切片对象的元组。
除了其处理切片表示法的独特能力外,索引操作与普通方法调用有相似之处:当仅用一个元素调用时,它表现得像一个方法调用;如果元素数量大于一个,则 idx 参数的行为类似于 *args。然而,正如在“动机”部分所述,索引操作具有从更大的集合中提取子集的强烈语义含义,这与常规方法调用不自动相关联,除非选择适当的命名。此外,其不同的视觉风格对于可读性也很重要。
规范
实现应尽量保留 __getitem__ 的当前签名,或以向后兼容的方式修改它。我们将提出不同的替代方案,同时考虑需要解决的各种情况。
C0. a[1]; a[1,2] # Traditional indexing
C1. a[Z=3]
C2. a[Z=3, R=4]
C3. a[1, Z=3]
C4. a[1, Z=3, R=4]
C5. a[1, 2, Z=3]
C6. a[1, 2, Z=3, R=4]
C7. a[1, Z=3, 2, R=4] # Interposed ordering
策略“严格字典”
此策略承认 __getitem__ 在只接受一个对象方面是特殊的,并且该对象的性质在其轴的规范中必须是非歧义的:可以通过顺序或名称来指定。因此,在这种假设下,存在关键字参数时,传递的实体是一个字典,并且所有标签都必须指定。
C0. a[1]; a[1,2] -> idx = 1; idx = (1, 2)
C1. a[Z=3] -> idx = {"Z": 3}
C2. a[Z=3, R=4] -> idx = {"Z": 3, "R": 4}
C3. a[1, Z=3] -> raise SyntaxError
C4. a[1, Z=3, R=4] -> raise SyntaxError
C5. a[1, 2, Z=3] -> raise SyntaxError
C6. a[1, 2, Z=3, R=4] -> raise SyntaxError
C7. a[1, Z=3, 2, R=4] -> raise SyntaxError
优点
- 元组情况和字典情况之间具有强烈的概念相似性。在第一种情况下,我们正在指定一个元组,因此我们自然地定义了一组由逗号分隔的普通值。在第二种情况下,我们正在指定一个字典,因此我们正在指定一组同质的键/值对,如
dict(Z=3, R=4); - 在
__getitem__端简单易于解析:如果它接收到一个元组,则使用位置来确定轴。如果它接收到一个字典,则使用关键字。 - C 接口不需要更改。
中立
- 将
a[{"Z": 3, "R": 4}]与a[Z=3, R=4]进行退化,意味着该表示法是语法糖。
缺点
- 非常严格。
- 破坏了传递参数的顺序。使用 PEP 468 中草拟的 OrderedDict 可以保留顺序。
- 不允许混合位置/关键字参数的用例,例如
a[1, 2, default=5]。
策略“混合字典”
此策略放宽了上述限制,返回一个包含数字和字符串作为键的字典。
C0. a[1]; a[1,2] -> idx = 1; idx = (1, 2)
C1. a[Z=3] -> idx = {"Z": 3}
C2. a[Z=3, R=4] -> idx = {"Z": 3, "R": 4}
C3. a[1, Z=3] -> idx = { 0: 1, "Z": 3}
C4. a[1, Z=3, R=4] -> idx = { 0: 1, "Z": 3, "R": 4}
C5. a[1, 2, Z=3] -> idx = { 0: 1, 1: 2, "Z": 3}
C6. a[1, 2, Z=3, R=4] -> idx = { 0: 1, 1: 2, "Z": 3, "R": 4}
C7. a[1, Z=3, 2, R=4] -> idx = { 0: 1, "Z": 3, 2: 2, "R": 4}
优点
- 为混合情况开放。
缺点
- 破坏了字符串键的顺序信息。我们无法确定“Z”在 C7 中的位置是 1 还是 3。
- 这意味着在遇到一个关键字参数时,从元组切换到字典。解析时可能令人困惑。
策略“命名元组”
为 idx 返回一个命名元组而不是一个元组。关键字参数显然会以其指定的名称作为键,而位置参数会以前导下划线加上其顺序来表示。
C0. a[1]; a[1,2] -> idx = 1; idx = (_0=1, _1=2)
C1. a[Z=3] -> idx = (Z=3)
C2. a[Z=3, R=2] -> idx = (Z=3, R=2)
C3. a[1, Z=3] -> idx = (_0=1, Z=3)
C4. a[1, Z=3, R=2] -> idx = (_0=1, Z=3, R=2)
C5. a[1, 2, Z=3] -> idx = (_0=1, _2=2, Z=3)
C6. a[1, 2, Z=3, R=4] -> (_0=1, _1=2, Z=3, R=4)
C7. a[1, Z=3, 2, R=4] -> (_0=1, Z=3, _1=2, R=4)
or (_0=1, Z=3, _2=2, R=4)
or raise SyntaxError
命名元组的所需类型名称可以是 Index 或函数定义中的参数名,它保留了顺序,并且易于通过 `_fields` 属性进行分析。它是向后兼容的,前提是 C0 中超过一个条目现在传递的是命名元组而不是普通元组。
优点
- 看起来不错。命名元组透明地替换了元组,并优雅地退化为旧行为。
- 不需要更改 C 接口。
缺点
- 根据 一些来源,命名元组未得到良好开发。将其包含为如此重要的对象可能需要返工和改进;
- 命名元组的字段,因此类型,将必须根据传递的参数而变化。这可能是一个性能瓶颈,并且无法保证两次连续的索引访问获得相同的 Index 类;
- `_n` “魔法”字段有些不寻常,但 ipython 已经使用它们来保存结果历史。
- Python 当前没有内置的命名元组。当前的命名元组在标准库的“collections”模块中可用。
- 与函数不同,
gridValues[x=3, y=5, z=8]和gridValues[3,5,8]这两种表示法在调用时修改顺序(例如,我们请求gridValues[y=5, z=8, x=3]`)时不会优雅地匹配。在函数中,我们可以预定义参数名称,以便正确匹配关键字参数。但在__getitem__中不行,将解释和匹配的任务留给了__getitem__本身。
策略“新的参数内容”
在当前实现中,当许多参数传递给 __getitem__ 时,它们会被分组到一个元组中,并将该元组作为单个参数 idx 传递给 __getitem__。此策略保留了当前签名,但扩展了 idx 的类型和内容的可变性范围,以适应更复杂的表示。
我们确定了实现此策略的四种可能方式:
- P1:使用单个字典来表示关键字参数。
- P2:使用单独的单项字典。
- P3:类似于 P2,但用
(key, value)元组替换单项字典。 - P4:类似于 P2,但使用一种特殊且附加的新对象:
keyword()。
其中一些可能性会导致表示法退化,即与已经可能的表示法无法区分。再次,提议的表示法成为这些表示法的语法糖。
在此策略下,C0 的旧行为保持不变。
C0: a[1] -> idx = 1 # integer
a[1,2] -> idx = (1,2) # tuple
在 C1 中,我们可以使用字典或元组来表示 C1 中键值对,用于特定的索引条目。我们需要一个带元组的元组,否则我们无法区分 a["Z", 3] 和 a[Z=3]。
C1: a[Z=3] -> idx = {"Z": 3} # P1/P2 dictionary with single key
or idx = (("Z", 3),) # P3 tuple of tuples
or idx = keyword("Z", 3) # P4 keyword object
正如你所见,表示法 P1/P2 意味着 a[Z=3] 和 a[{"Z": 3}] 将调用 __getitem__ 并传递完全相同的值,因此它是后者的语法糖。对于 P3,也发生相同的情况,尽管索引不同。使用像 P4 这样的关键字对象将消除这种退化。
对于 C2 情况:
C2. a[Z=3, R=4] -> idx = {"Z": 3, "R": 4} # P1 dictionary/ordereddict
or idx = ({"Z": 3}, {"R": 4}) # P2 tuple of two single-key dict
or idx = (("Z", 3), ("R", 4)) # P3 tuple of tuples
or idx = (keyword("Z", 3),
keyword("R", 4) ) # P4 keyword objects
P1 自然地映射到传统的 **kwargs 行为,但它破坏了两个或更多条目生成元组的约定。P2 保留了此行为,并额外保留了顺序。使用 PEP 468 中草拟的 OrderedDict 也可以保留顺序。
其余情况在此处显示:
C3. a[1, Z=3] -> idx = (1, {"Z": 3}) # P1/P2
or idx = (1, ("Z", 3)) # P3
or idx = (1, keyword("Z", 3)) # P4
C4. a[1, Z=3, R=4] -> idx = (1, {"Z": 3, "R": 4}) # P1
or idx = (1, {"Z": 3}, {"R": 4}) # P2
or idx = (1, ("Z", 3), ("R", 4)) # P3
or idx = (1, keyword("Z", 3),
keyword("R", 4)) # P4
C5. a[1, 2, Z=3] -> idx = (1, 2, {"Z": 3}) # P1/P2
or idx = (1, 2, ("Z", 3)) # P3
or idx = (1, 2, keyword("Z", 3)) # P4
C6. a[1, 2, Z=3, R=4] -> idx = (1, 2, {"Z":3, "R": 4}) # P1
or idx = (1, 2, {"Z": 3}, {"R": 4}) # P2
or idx = (1, 2, ("Z", 3), ("R", 4)) # P3
or idx = (1, 2, keyword("Z", 3),
keyword("R", 4)) # P4
C7. a[1, Z=3, 2, R=4] -> idx = (1, 2, {"Z": 3, "R": 4}) # P1. Pack the keyword arguments. Ugly.
or raise SyntaxError # P1. Same behavior as in function calls.
or idx = (1, {"Z": 3}, 2, {"R": 4}) # P2
or idx = (1, ("Z", 3), 2, ("R", 4)) # P3
or idx = (1, keyword("Z", 3),
2, keyword("R", 4)) # P4
优点
- 签名保持不变;
- P2/P3 可以保留索引时指定的关键字参数的顺序,
- P1 需要 OrderedDict,但如果允许,它会破坏插入顺序:所有关键字索引都将被转储到字典中;
- 保留在传统类型内:元组和字典。可选 OrderedDict;
- 一些提出的策略在行为上与传统的函数调用相似;
- 用于 `PyObject_GetItem` 及其系列的 C 接口将保持不变。
缺点
- 显然复杂且浪费;
- 表示法的退化(例如,
a[Z=3]和a[{"Z":3}]在__[get|set|del]item__级别是等价且无法区分的)。这种行为可能被接受,也可能不被接受。 - 对于 P4,需要一个类似于 `slice()` 的附加对象,但仅用于消除上述退化的歧义。
- `idx` 的类型和布局似乎取决于调用者的随意决定;
- 解析传递的内容可能很复杂,尤其是在元组的元组情况下;
- P2 创建了大量作为元组成员的单键字典。看起来很丑陋。P3 会比字典元组更轻便易用,并且仍然保留顺序(与常规字典不同),但会导致关键字提取笨拙。
策略“kwargs参数”
`__getitem__` 接受一个可选的 `**kwargs` 参数,该参数应该是仅关键字的。`idx` 也变为可选,以支持不允许非关键字参数的情况。然后签名将是:
__getitem__(self, idx)
__getitem__(self, idx, **kwargs)
__getitem__(self, **kwargs)
应用于我们的情况将产生:
C0. a[1,2] -> idx=(1,2); kwargs={}
C1. a[Z=3] -> idx=None ; kwargs={"Z":3}
C2. a[Z=3, R=4] -> idx=None ; kwargs={"Z":3, "R":4}
C3. a[1, Z=3] -> idx=1 ; kwargs={"Z":3}
C4. a[1, Z=3, R=4] -> idx=1 ; kwargs={"Z":3, "R":4}
C5. a[1, 2, Z=3] -> idx=(1,2); kwargs={"Z":3}
C6. a[1, 2, Z=3, R=4] -> idx=(1,2); kwargs={"Z":3, "R":4}
C7. a[1, Z=3, 2, R=4] -> raise SyntaxError # in agreement to function behavior
空索引 a[] 当然仍然是无效语法。
优点
- 类似于函数调用,自然演变而来;
- 使用关键字索引的对象,其 `__getitem__` 没有 kwargs,将以明显的方式失败。其他策略则不是这种情况。
缺点
- 它不保留顺序,除非使用 OrderedDict;
- 禁止 C7,但真的有必要吗?
- 需要更改 C 接口以传递一个额外的 PyObject 来表示关键字参数。
C 接口
如前分析所述,C 接口可能会为了支持新功能而进行更改。具体来说,`PyObject_GetItem` 及其相关例程将不得不接受一个额外的 `PyObject *kw` 参数用于策略“kwargs 参数”。其余策略不需要更改 C 函数签名,但传递对象的不同性质可能需要调整。
策略“命名元组”无需任何更改即可正确运行:collections 中工厂方法返回的类是元组的子类,这意味着 `PyTuple_*` 函数可以处理结果对象。
替代解决方案
在本节中,我们提出了替代解决方案,这些解决方案可以解决缺失的功能,并使得提议的增强功能不值得实现。
使用方法
可以保持索引不变,并使用传统的 `get()` 方法来处理基本索引不足的情况。这是一个不错的观点,但正如在引言中已经提到的,方法比索引具有不同的语义权重,而且你不能直接在方法中使用切片。例如,比较 a[1:3, Z=2] 和 a.get(slice(1,3), Z=2)。
然而,作者们认为这一论点很有说服力,并且基于关键字的索引在语义表达上的优势可能会被一个很少使用且好处不大的功能所抵消,并且可能采用有限。
通过滥用切片对象来模拟所需行为
这种极富创意的技术利用了切片对象行为,前提是接受使用字符串(或实例化适当命名的占位符对象作为键),并接受使用“:”而不是“=”。
>>> a["K":3]
slice('K', 3, None)
>>> a["K":3, "R":4]
(slice('K', 3, None), slice('R', 4, None))
>>>
虽然这个方法很聪明,但它不允许轻松查询键/值对,它太巧妙和晦涩,并且不允许像 a[K=1:10:2] 这样传递切片。
然而,Tim Delaney评论说:
“我真的认为a[b=c, d=e]应该只是a['b':c, 'd':e]的语法糖。这很容易解释,并且提供了最大的向后兼容性。特别是,已经以这种方式滥用切片功能的库将继续在新语法下运行。”
我们认为这种行为会产生不方便的结果。Pandas 库使用字符串作为标签,允许使用 df.ix["A":"F"] 这样的表示法。
>>> a[:, "A":"F"]
从列“A”到列“F”提取数据。根据上述评论,这种表示法也可以通过以下方式获得:
>>> a[:, A="F"]
这很奇怪,并且与关键字在索引中的本意相冲突,即通过约定俗成的名称而不是位置来指定轴。
将字典作为附加索引传递
>>> a[1, 2, {"K": 3}]
这种表示法虽然不够优雅,但已经可以使用并且可以达到类似的结果。很明显,提议的策略“新的参数内容”可以被解释为这种表示法的语法糖。
补充说明
评论者还提出了以下相关观点:
关键字参数顺序的相关性
作为讨论此 PEP 的一部分,重要的是决定关键字参数的顺序信息是否重要,以及索引和键是否可以以任意方式排序(例如 a[1,Z=3,2,R=4])。PEP 468 试图通过提议使用 ordereddict 来解决第一个问题,然而人们会倾向于接受关键字参数在索引中等同于函数调用中的 kwargs,因此截至今天同样是无序的,并且具有相同的限制。
行为同质性的需求
相对于策略“新的参数内容”,Ian Cordasco 的评论指出:
“对于只有一个方法与 Python 中的标准行为完全不同,这是不合理的。仅__getitem__(以及表面上__setitem__)接受关键字参数,但不是将它们转换为字典,而是将它们转换为单独的单项字典,这会令人困惑。”我们同意他的观点,但必须指出__getitem__在传递参数方面已经有些特殊。
Chris Angelico 也表示:
“一开始说‘好的,让我们让索引有机会携带关键字参数,就像函数调用一样’,然后又说‘哦,但与函数调用不同,它们本质上是有序的,并且传递方式非常不同’,这似乎很奇怪。”我们同意这一点。保持同质性最直接的策略是策略“kwargs 参数”,它为 `__getitem__` 打开一个 `**kwargs` 参数。
其中一位作者(Stefano Borini)认为只有“严格字典”策略值得实现。它非歧义、简单、不强制复杂的解析,并解决了通过位置或名称引用轴的问题。“选项”用例可能最好通过不同的方法来处理,并且可能与此 PEP 无关。“命名元组”替代方案是另一个可行的选择。
使 .get() 方法因带有默认回退的索引而过时
引入“默认”关键字可以使 `dict.get()` 过时,它将被 `d["key", default=3]` 取代。然而,Chris Angelico 表示:
“目前,您需要编写 `__getitem__`(在找到问题时引发异常)加上其他东西,例如 `get()`,它返回一个默认值。根据您的建议,两个分支都将进入 `__getitem__`,这意味着它们可以共享代码;但仍然需要两个分支。”
此外,Chris 继续说道:
“将有一个临时的、相当随意的名称集合(有些会写 `default=`,有些会说太长了就写 `def=`,但那是一个关键字,所以他们会写 `dflt=` 或其他东西……),除非有强大的力量促使人们选择一个一致的名称。”
这个论点是有效的,但对于任何函数调用也是同样有效的,并且通常由既定的约定和文档来解决。
关于表示法的退化
用户 Drekin 评论说:“a[Z=3] 和 a[{"Z": 3}] 的情况类似于当前的 a[1, 2] 和 a[(1, 2)]。即使有人可能认为括号实际上不是元组表示法的一部分,而只是因为语法而需要,但与函数调用相比,这可能看起来是表示法的退化:f(1, 2) 与 f((1, 2)) 不同。”
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0472.rst