抽象
唯一占位符值,通常称为“哨兵值”,常见于 编程。它们有很多用途,例如:
函数参数的默认值,用于未给出值时:
def foo(value=None): ...
当找不到某些内容或不可用时,从函数返回值:
>>> "abc".find("d")-1
缺少数据,例如关系数据库中的 NULL 或“N/A”(“not avAIlable“) 在电子表格中
Python 具有 特殊值 ,旨在用作 在大多数情况下是哨兵值。然而,有时是替代哨兵 值是必需的,通常当它需要与 不同时。这些 案例很常见,以至于实现此类哨兵的几个成语 多年来一直出现,但并不常见,以至于没有 明确需要标准化。但是,常见的实现, 包括 stdlib 中的一些,有几个明显的缺点。NoneNone
此 PEP 建议添加一个用于定义哨兵值的实用程序,以供使用 在 stdlib 中,并作为 stdlib 的一部分公开提供。
注意:更改 stdlib 中要实现的所有现有哨兵 方式不被认为是必要的,是否这样做由自行决定 的维护者。
赋予动机
2021 年 5 月,python-dev 邮件列表 上提出了一个关于如何更好地实现 .现有实现使用了 以下常见成语:traceback.print_exception
_sentinel = object()
但是,此对象具有信息量不足且过于冗长的 repr,导致 函数的签名过长且难以阅读:
>>> help(traceback.print_exception)Help on function print_exception in module traceback:print_exception(exc, /, value=<object object at0x000002825DF09650>, tb=<object object at 0x000002825DF09650>,limit=None, file=None, chain=True)
此外,还提出了许多现有哨兵的另外两个缺点 在讨论中:
没有明显的类型,因此无法定义清晰 以 Sentinels 为默认值的函数的类型签名
复制或取消腌制后的错误行为,由于单独的 正在创建的实例,因此使用 failure 进行比较is
在随后的讨论中,Victor Stinner 提供了当前使用的清单 Python 标准库 中的 sentinel 值。这表明 对哨兵的需求是相当普遍的,有各种实现 甚至在 stdlib 中使用的方法,其中许多方法都受到 at 上述三个缺点中的至少一个。
讨论没有就标准是否达成任何明确的共识 实现方法是需要的还是可取的,无论是否提到缺点 是重要的,也不是哪种实现是好的。作者 的这个 PEP 在 bugs.python.org 上创建了一个问题,建议 改进,但只关注少数几个有问题的方面 案件,未能获得任何支持。
在 discuss.python.org 上创建了一个民意调查,以更清楚地了解 社区的意见。民意调查的结果不是决定性的,只有40% 投票支持“现状很好/不需要一致性 这“,但大多数选民投票支持一种或多种标准化解决方案。 具体来说,37%的选民选择了“始终如一地使用新的、专用的 Sentinel Factory / Class / Meta-Class,也在 stdlib”。
由于意见不一,这个 PEP 的创建是为了促进做出决定 关于这个主题。
在处理此 PEP 时,迭代各种选项和实现 并不断讨论,笔者得出的结论是,一个简单的, 标准库中可用的良好实现是值得拥有的, 既可用于标准库本身,也可用于其他地方。
理由
指导所选实施的标准是:
哨兵对象的行为应与哨兵对象预期的行为相同: 当 与使用运算符相比,应始终考虑 与自身相同,但从不与任何其他对象相同。is
创建哨兵对象应该是一个简单、直接的单行。
根据需要定义尽可能多的不同哨兵值应该很简单。
哨兵对象应具有清晰而简短的 repr。
应该可以对哨兵使用明文类型签名。
哨兵对象在复制和/或复制后应表现正常 不腌制。
这样的哨兵在使用 CPython 3.x 和 PyPy3 时应该可以工作,理想情况下 也适用于 Python 的其他实现。
在实施中,尤其是尽可能简单明了 使用中。避免这成为学习时要学习的一件更特别的事情 蟒。它应该在需要时易于查找和使用,并且足够明显 在阅读代码时,人们通常不会觉得有必要查找其 文档。
由于 Python 标准库 中有如此多的用途,因此 在标准库中有一个实现,因为 stdlib 不能使用 其他位置可用的 sentinel 对象的实现(例如 或 PyPI 包)。sentinelssentinel
在研究了现有的习语和实现之后,并经历了许多 不同的可能实现,编写了一个满足 所有这些标准(请参阅参考下面的实现)。
一个新类将添加到新模块中。 它的初始值设定项将接受一个必需的参数,即 Sentinel 对象和两个可选参数:对象的 repr 和 其模块名称:Sentinelsentinels
>>> from sentinels import Sentinel>>> NotGiven = Sentinel('NotGiven')>>> NotGiven<NotGiven>>>> MISSING = Sentinel('MISSING', repr='mymodule.MISSING')>>> MISSINGmymodule.MISSING>>> MEGA = Sentinel('MEGA', repr='<MEGA>', module_name='mymodule')<MEGA>
检查值是否为这样的哨兵应使用运算符完成,建议用于 。使用意志进行平等检查 也按预期工作,仅在比较对象时返回 与它自己。身份检查,例如通常应该 而不是布尔检查,例如 或 。 默认情况下,Sentinel 实例是真实的。isNone==Trueif value is MISSING:if value:if not value:
哨兵的名称在每个模块中都是唯一的。在调用已经具有该名称的哨兵的模块时 定义时,将返回具有该名称的现有哨兵。哨兵 在不同的模块中具有相同的名称将彼此不同。Sentinel()
创建哨兵对象的副本,例如 using 或 酸洗和取消腌制,将返回相同的对象。copy.copy()
哨兵值的类型注释应使用 。例如:Sentinel
def foo(value: int | Sentinel = MISSING) -> int: ...
通常不需要提供可选参数, 因为通常能够识别它所在的模块 叫。 只有在特殊情况下才应提供 自动识别无法按预期工作,例如在使用 Jython 或 IronPython。这与 和 的设计相似。有关详细信息,请参阅 PEP 435。module_nameSentinel()module_nameEnumnamedtuple
该类可以被子类化。每个子类的实例将 保持唯一,即使使用相同的名称和模块。这允许 自定义哨兵的行为,例如控制其真实性。Sentinel
参考实现
参考实现可在专用的 Github 存储库 中找到。一个 简化版本如下:
_registry = {}class Sentinel: """Unique sentinel values.""" def __new__(cls, name, repr=None, module_name=None): name = str(name) repr = str(repr) if repr else f'<{name.split(".")[-1]}>' if module_name is None: try: module_name = \ sys._getframe(1).f_globals.get('__name__', '__main__') except (AttributeError, ValueError): module_name = __name__ registry_key = f'{module_name}-{name}' sentinel = _registry.get(registry_key, None) if sentinel is not None: return sentinel sentinel = super().__new__(cls) sentinel._name = name sentinel._repr = repr sentinel._module_name = module_name return _registry.setdefault(registry_key, sentinel) def __repr__(self): return self._repr def __reduce__(self): return ( self.__class__, ( self._name, self._repr, self._module_name, ), )
被拒绝的想法
用
这存在“基本原理”部分提到的所有缺点。
添加单个新的哨兵值,例如
由于这样的值可以用于不同地方的各种事物,因此一 不能总是确信它在某些用途中永远不会是有效值 例。另一方面,可以使用专用且独特的哨兵值 充满信心,无需考虑潜在的边缘情况。
此外,能够提供有意义的名称和 repr 也很有用 对于特定于使用它的上下文的 Sentinel 值。
最后,这在民意调查中是一个非常不受欢迎的选择,只有12% 投票赞成票的票数。
使用现有的 sentinel 值
这不是省略号的最初预期用途,尽管它已成为 用它来定义空类或功能块越来越普遍 的使用 .pass
此外,与潜在的新单个哨兵值类似,不能是 在所有情况下都自信地使用,这与专用的、独特的价值不同。Ellipsis
使用单值枚举
建议的成语是:
class NotGivenType(Enum): NotGiven = 'NotGiven'NotGiven = NotGivenType.NotGiven
除了过度重复之外,repr 也过长:.可以定义一个较短的 repr,在 多一点代码和更多重复的代价。<NotGivenType.NotGiven: 'NotGiven'>
最后,这个选项是九个选项中最不受欢迎的 民意调查 ,是唯一获得无票的选择。
哨兵类装饰器
建议的成语是:
@sentinel(repr='<NotGiven>')class NotGivenType: passNotGiven = NotGivenType()
虽然这允许非常简单明了地实现装饰器, 这个成语太冗长,重复,很难记住。
使用类对象
由于类本质上是单例,因此使用类作为哨兵值 有意义,并允许简单的实现。
最简单的版本是:
class NotGiven: pass
要获得明确的 repr,需要使用元类:
class NotGiven(metaclass=SentinelMeta): pass
...或类装饰器:
@Sentinelclass NotGiven: pass
以这种方式使用类是不寻常的,可能会造成混淆。意图 如果没有注释,代码将很难理解。它还会导致 这样的哨兵有一些意想不到的和不受欢迎的行为,例如 是可调用的。
定义一个推荐的“标准”习语,而不提供实现
大多数常见的现有习语都有明显的缺点。到目前为止,还没有成语 已经发现,在避免这些缺点的同时,清晰简洁。
此外,在关于此主题的民意调查 中,推荐 成语不受欢迎,得票最高的选项只有 25%的选民。
每个哨兵值的特定类型签名
很长一段时间以来,这个 PEP 的作者都努力为 特定于每个值的此类哨兵。领先的提案 (由 Guido 等人支持)是扩大 的使用 ,例如 .经过深思熟虑和讨论,尤其是在 typing-sig mailing list ,似乎所有这些解决方案都需要 特殊大小写和/或增加了静态类型实现的复杂性 跳棋,同时也限制了哨兵的实施。LiteralLiteral[MISSING]
因此,此 PEP 不再提出此类签名。取而代之的是,这个 PEP 建议用作 Sentinel 值的类型签名。Sentinel
有点不幸的是,静态类型检查器有时不会 因此能够推断出更具体的类型,例如在条件 块,如.但是,这是一个小问题 在实践中,可以很容易地进行类型检查器来理解这些情况 用。if value is not MISSING: ...typing.cast()
附注事项
此 PEP 和初始实现是在专用 GitHub 中起草的 回购 。
对于在类作用域中定义的哨兵,为避免潜在的名称冲突, 应该使用模块中变量的完全限定名称。只 最后一个句点之后的名称部分将用于默认值 repr.例如:
>>> class MyClass:... NotGiven = sentinel('MyClass.NotGiven')>>> MyClass.NotGiven<NotGiven>
在函数或方法中创建哨兵时应小心,因为 由同一模块中的代码创建的同名哨兵将是 相同。如果需要不同的哨兵对象,请确保使用 不同的名称。
版权
本文档被置于公共领域或位于 CC0-1.0-通用许可证,以更宽松的许可证为准。
文章声明:以上内容(如有图片或视频在内)除非注明,否则均为腾龙猫勺儿原创文章,转载或复制请以超链接形式并注明出处。
本文作者:猫勺本文链接:https://www.jo6.cn/post/60.html
还没有评论,来说两句吧...