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

Python 增强提案

PEP 769 – 为 ‘attrgetter’、‘itemgetter’ 和 ‘getitem’ 添加 ‘default’ 关键字参数

作者:
Facundo Batista <facundo at taniquetil.com.ar>
讨论至:
Discourse 帖子
状态:
已拒绝
类型:
标准跟踪
创建日期:
2024 年 12 月 22 日
Python 版本:
3.14
发布历史:
2025 年 1 月 7 日
决议:
2025年3月14日

目录

摘要

本提案旨在通过向 attrgetteritemgettergetitem 函数添加 default 关键字参数来增强 operator 模块。此添加允许这些函数在目标属性或项缺失时返回指定的默认值,从而防止异常并简化处理可选属性或项的代码。

动机

当前,如果指定的属性或项不存在,attrgetteritemgetter 会引发异常。此限制要求开发人员实现额外的错误处理,导致代码更复杂且可读性更差。

引入 default 参数将简化涉及可选属性或项的操作,减少样板代码并提高代码清晰度。

getitem 类似的情况也会发生,并且增加的细微之处在于,允许在这种情况下指定默认值将解决与 getattr() 内置函数长期存在的不一致性。

基本原理

主要设计决策是引入一个适用于所有指定属性或项的单个 default 参数。

这种方法保持了简洁性,并避免了为多个属性或项分配单独默认值的复杂性。虽然一些讨论曾考虑允许多个默认值,但增加的复杂性和潜在的混淆导致倾向于所有情况下的单个默认值(更多关于此内容,请参见下文 被拒绝的想法)。

规范

建议的行为

  • attrgetterf = attrgetter("name", default=XYZ) 后跟 f(obj),如果属性存在,则返回 obj.name,否则返回 XYZ
  • itemgetterf = itemgetter(2, default=XYZ) 后跟 f(obj),如果 obj[2] 有效,则返回 obj[2],否则返回 XYZ
  • getitemgetitem(obj, k, XYZ)getitem(obj, k, default=XYZ),如果 obj[k] 有效,则返回 obj[k],否则返回 XYZ

在前两种情况下,此增强适用于单个和多个属性/项的检索,对于任何缺失的属性或项,都将返回默认值。

如果在任何情况下都不使用额外的默认(关键字)参数,则不包含功能更改。

attrgetter 的示例

当前行为保持不变

>>> class C:
...   class D:
...     class X:
...       pass
...   class E:
...     pass
...
>>> attrgetter("D")(C)
<class '__main__.C.D'>
>>> attrgetter("badname")(C)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'C' has no attribute 'badname'
>>> attrgetter("D", "E")(C)
(<class '__main__.C.D'>, <class '__main__.C.E'>)
>>> attrgetter("D", "badname")(C)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'C' has no attribute 'badname'
>>> attrgetter("D.X")(C)
<class '__main__.C.D.X'>
>>> attrgetter("D.badname")(C)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'D' has no attribute 'badname'

使用此 PEP,使用建议的 default 关键字

>>> attrgetter("D", default="noclass")(C)
<class '__main__.C.D'>
>>> attrgetter("badname", default="noclass")(C)
'noclass'
>>> attrgetter("D", "E", default="noclass")(C)
(<class '__main__.C.D'>, <class '__main__.C.E'>)
>>> attrgetter("D", "badname", default="noclass")(C)
(<class '__main__.C.D'>, 'noclass')
>>> attrgetter("D.X", default="noclass")(C)
<class '__main__.C.D.X'>
>>> attrgetter("D.badname", default="noclass")(C)
'noclass'

itemgetter 的示例

当前行为保持不变

>>> obj = ["foo", "bar", "baz"]
>>> itemgetter(1)(obj)
'bar'
>>> itemgetter(5)(obj)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> itemgetter(1, 0)(obj)
('bar', 'foo')
>>> itemgetter(1, 5)(obj)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

使用此 PEP,使用建议的 default 关键字

>>> itemgetter(1, default="XYZ")(obj)
'bar'
>>> itemgetter(5, default="XYZ")(obj)
'XYZ'
>>> itemgetter(1, 0, default="XYZ")(obj)
('bar', 'foo')
>>> itemgetter(1, 5, default="XYZ")(obj)
('bar', 'XYZ')

getitem 的示例

当前行为保持不变

>>> obj = ["foo", "bar", "baz"]
>>> getitem(obj, 1)
'bar'
>>> getitem(obj, 5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

使用此 PEP,使用建议的额外默认值,按位置或关键字

>>> getitem(obj, 1, "XYZ")
'bar'
>>> getitem(obj, 5, "XYZ")
'XYZ'
>>> getitem(obj, 1, default="XYZ")
'bar'
>>> getitem(obj, 5, default="XYZ")
'XYZ'

关于可能的实现

attrgetter 的实现非常直接:它涉及使用 getattr 并捕获可能的 AttributeError。因此 attrgetter("name", default=XYZ)(obj) 将类似于

try:
    value = getattr(obj, "name")
except AttributeError:
    value = XYZ

请注意,我们不能依赖使用带默认值的 getattr,因为当指定属性链(例如 attrgetter("foo.bar.baz", default=XYZ))时,无法区分每一步返回的内容。

itemgettergetitem 的实现并非如此简单。更直接的方法同样简单易懂:尝试 __getitem__ 并捕获可能的异常(见下文)。这样,itemgetter(123, default=XYZ)(obj)getitem(obj, 123, default=XYZ) 将等同于

try:
    value = obj[123]
except (IndexError, KeyError):
    value = XYZ

然而,出于性能原因,实现可能看起来更像以下内容,它具有完全相同的行为

if type(obj) == dict:
    value = obj.get(123, XYZ)
else:
    try:
        value = obj[123]
    except (IndexError, KeyError):
        value = XYZ

注意,验证是关于确切类型而不是使用 isinstance;这是为了确保确切的行为,如果对象是用户定义的、继承自 dict 但重写了 get 的对象,这将是不可能的(原因与检查对象是否具有 get 方法类似)。

这样,性能更好,但这只是一个实现细节,因此我们可以保留关于其行为的原始解释。

关于要捕获的异常,即使 __getitem__ 可以引发 IndexErrorKeyErrorTypeError(参见其 参考),只有前两者可能发生在容器不包含指定键或索引时,而后者很可能表示代码中的错误,因此我们不捕获它来触发默认行为。

边界情况

提供 default 选项仅在访问项/属性在正常情况下会失败时才有效。换句话说,被访问的对象本身不应处理默认值。

例如,以下情况将是冗余/令人困惑的,因为 defaultdict 在访问项时永远不会出错

>>> from collections import defaultdict
>>> from operator import itemgetter
>>> dd = defaultdict(int)
>>> itemgetter("foo", default=-1)(dd)
0

这同样适用于任何重载 __getitem____getattr__ 并实现其自身回退的用户定义对象。

被拒绝的想法

多个默认值

曾考虑过允许为多个属性或项提供多个默认值的想法。

讨论了两种替代方案:使用与传递给 attrgetter/itemgetter 的参数数量相同的可迭代对象,或者使用键与传递给 attrgetter/itemgetter 的名称匹配的字典。

这里真正复杂的问题(使功能难以解释且具有令人困惑的边界情况)是,如果可迭代对象或字典是所有项的*实际*期望默认值,会发生什么。例如

>>> itemgetter("a", default=(1, 2))({})
(1, 2)
>>> itemgetter("a", "b", default=(1, 2))({})
((1, 2), (1, 2))

如果我们允许使用 default 给出“多个默认值”,则上面示例中的第一种情况将引发异常,因为项的数量多于默认值中的名称,而第二种情况将返回 (1, 2))。这就是为什么我们考虑使用不同的名称来表示多个默认值(例如 defaults,这是富有表现力的,但可能因为与 default 太相似而容易出错)。

另一个支持多个默认值的提案是允许 attrgetteritemgetter 的组合,例如

>>> ig_a = itemgetter("a", default=1)
>>> ig_b = itemgetter("b", default=2)
>>> ig_combined = itemgetter(ig_a, ig_b)
>>> ig_combined({"a": 999})
(999, 2)
>>> ig_combined({})
(1, 2)

然而,组合 itemgetterattrgetter 是全新的行为,并且非常复杂。虽然并非不可能,但这超出了本 PEP 的范围。

最终,认为多个默认值过于复杂且可能令人困惑,因此为了简洁性和可预测性,倾向于使用单个 default 参数。

元组返回一致性

另一个被拒绝的提案是添加一个标志,始终返回一个元组,无论给定了多少键/名称/索引。例如

>>> letters = ["a", "b", "c"]
>>> itemgetter(1, return_tuple=True)(letters)
('b',)
>>> itemgetter(1, 2, return_tuple=True)(letters)
('b', 'c')

这对于多个默认值的一致性帮助不大,需要进一步讨论,并且超出了本 PEP 的范围。

未解决的问题

目前没有未解决的问题。

如何教授此内容

由于基本行为未被修改,因此在首次教授 attrgetteritemgetter 时可以避免使用此 default。只有在需要该功能时才能引入它。

向后兼容性

拟议的更改向后兼容。 default 参数是可选的;没有此参数的现有代码将按原样运行。只有明确使用新的 default 参数的代码才会表现出新行为,从而确保不会中断当前实现。

安全隐患

引入 default 参数本身不会引入安全漏洞。


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

最后修改:2025-05-26 08:01:11 GMT