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

Python增强提案

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 将探讨启用这些表示法的不同策略。

用例

以下实际用例展示了关键字规范的两种广泛使用类别:索引和上下文选项。对于索引

  1. 为索引提供更具表达力的含义,例如防止意外反转索引
    >>> gridValues[x=3, y=5, z=8]
    >>> rain[time=0:12, location=location]
    
  2. 在某些领域,例如计算物理和化学,使用 Basis[Z=5] 这样的表示法是表示精度级别的领域特定语言表示法
    >>> low_accuracy_energy = computeEnergy(molecule, BasisSet[Z=3])
    

    在这种情况下,索引操作将返回在选定精度级别(由参数 Z 表示)处的基组。索引背后的原因是 BasisSet 对象可以在内部表示为数字表,其中行(“系数”轴,在本例中对用户隐藏)与单个元素关联(例如,行 0:5 包含元素 1 的系数,行 5:8 包含元素 2 的系数),并且每一列与给定的精度级别(“精度”或“Z”轴)关联,以便第一列是低精度,第二列是中精度,依此类推。使用该索引,用户将获得另一个表示内部表中精度级别 3 的列内容的对象。

此外,关键字规范可用作索引的上下文选项。具体来说

  1. “默认”选项允许在索引不存在时指定默认返回值
    >>> lst = [1, 2, 3]
    >>> value = lst[5, default=0]  # value is 0
    
  2. 对于稀疏数据集,指定插值策略以从例如其周围数据推断缺失点。
    >>> value = array[1, 3, interpolate=spline_interpolator]
    
  3. 可以使用相同的机制指定单位
    >>> 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])将生成切片对象,如果传递了多个值,则生成包含切片对象的元组。

除了其处理切片表示法的独特能力外,索引操作与普通方法调用类似:当仅使用一个元素调用时,它就像一个方法调用;如果元素数量大于 1,则 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}

优点

  • 开放混合用例。

缺点

  • 破坏字符串键的顺序信息。我们无法确定 C7 中的 "Z" 位于位置 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 接口

缺点

  • 根据一些来源 [4],命名元组尚未得到很好的开发。将它包含为如此重要的对象可能需要重新设计和改进;
  • 命名元组字段,以及类型,将不得不根据传递的参数进行更改。这可能是性能瓶颈,并且无法保证两次后续的索引访问获得相同的 Index 类;
  • 这里的 _n “魔法” 字段有点不寻常,但 ipython 已经将它们用于结果历史记录。
  • Python 目前没有内置的 namedtuple。当前版本可以在标准库的“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 库使用字符串作为标签,允许使用以下表示法

>>> 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

上次修改:2023-09-09 17:39:29 GMT