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日
摘要
本提案旨在通过向 attrgetter
、itemgetter
和 getitem
函数添加 default
关键字参数来增强 operator
模块。此添加允许这些函数在目标属性或项缺失时返回指定的默认值,从而防止异常并简化处理可选属性或项的代码。
动机
当前,如果指定的属性或项不存在,attrgetter
和 itemgetter
会引发异常。此限制要求开发人员实现额外的错误处理,导致代码更复杂且可读性更差。
引入 default
参数将简化涉及可选属性或项的操作,减少样板代码并提高代码清晰度。
与 getitem
类似的情况也会发生,并且增加的细微之处在于,允许在这种情况下指定默认值将解决与 getattr()
内置函数长期存在的不一致性。
基本原理
主要设计决策是引入一个适用于所有指定属性或项的单个 default
参数。
这种方法保持了简洁性,并避免了为多个属性或项分配单独默认值的复杂性。虽然一些讨论曾考虑允许多个默认值,但增加的复杂性和潜在的混淆导致倾向于所有情况下的单个默认值(更多关于此内容,请参见下文 被拒绝的想法)。
规范
建议的行为
- attrgetter:
f = attrgetter("name", default=XYZ)
后跟f(obj)
,如果属性存在,则返回obj.name
,否则返回XYZ
。 - itemgetter:
f = itemgetter(2, default=XYZ)
后跟f(obj)
,如果obj[2]
有效,则返回obj[2]
,否则返回XYZ
。 - getitem:
getitem(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)
)时,无法区分每一步返回的内容。
itemgetter
和 getitem
的实现并非如此简单。更直接的方法同样简单易懂:尝试 __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__
可以引发 IndexError
、KeyError
或 TypeError
(参见其 参考),只有前两者可能发生在容器不包含指定键或索引时,而后者很可能表示代码中的错误,因此我们不捕获它来触发默认行为。
边界情况
提供 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
太相似而容易出错)。
另一个支持多个默认值的提案是允许 attrgetter
和 itemgetter
的组合,例如
>>> 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)
然而,组合 itemgetter
或 attrgetter
是全新的行为,并且非常复杂。虽然并非不可能,但这超出了本 PEP 的范围。
最终,认为多个默认值过于复杂且可能令人困惑,因此为了简洁性和可预测性,倾向于使用单个 default
参数。
元组返回一致性
另一个被拒绝的提案是添加一个标志,始终返回一个元组,无论给定了多少键/名称/索引。例如
>>> letters = ["a", "b", "c"]
>>> itemgetter(1, return_tuple=True)(letters)
('b',)
>>> itemgetter(1, 2, return_tuple=True)(letters)
('b', 'c')
这对于多个默认值的一致性帮助不大,需要进一步讨论,并且超出了本 PEP 的范围。
未解决的问题
目前没有未解决的问题。
如何教授此内容
由于基本行为未被修改,因此在首次教授 attrgetter
和 itemgetter
时可以避免使用此 default
。只有在需要该功能时才能引入它。
向后兼容性
拟议的更改向后兼容。 default
参数是可选的;没有此参数的现有代码将按原样运行。只有明确使用新的 default
参数的代码才会表现出新行为,从而确保不会中断当前实现。
安全隐患
引入 default
参数本身不会引入安全漏洞。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0769.rst