PEP 661"哨兵值 "最近引起了人们对哨兵对象模式的关注**。**1
虽然这绝不是什么新鲜事2,但这次该模式出现在**打字**的上下文中,所以值得看看这两者是如何互动的。
内容。
什么是哨兵,为什么我需要一个哨兵? #
PEP 661的摘要对其进行了最好的总结。
唯一的占位符值,广为人知的 "哨兵值",在Python程序中对一些事情很有用,比如在None是有效输入值的情况下作为函数参数的默认值。
我能想到的最简单的用例是一个函数,它只在_明确_提供的情况下返回一个缺省值,否则会引发一个异常。
next()内置函数就是一个很好的例子。
next(iterator[, default])通过调用其
__next__()方法从_迭代器中_检索下一个项目。如果给定_默认值_,则在迭代器耗尽时返回,否则会引发StopIteration。
鉴于这个定义,让我们试着重新实现它。
next()本质上有两个签名3。
next(iterator)-> 项目或引发异常next(iterator, default)-> 项目或默认
有两种主要的方法来写一个支持这两种签名的函数。
next(*args, **kwargs); 你必须从_args_和_kwargs_ 中提取_iterator_和_default_,如果有太多/太少/意外的参数,则引发 TypeError。next(iterator, default=None);Python会检查参数,你只需要检查是否有参数。default is None
对我来说,第二个版本似乎比第一个版本更容易实现。
但是第二个版本有一个问题:对于一些用户来说,None 是一个有效的默认值--next() 如何区分_raise-exception-None和_default-value-None ?
在你自己的代码中,你可能能够保证None 永远不是一个有效的值,这就不是一个问题。
然而,在一个库中,你不想以这种方式限制用户,因为你通常不能预见他们所有的使用情况。 即使你选择像这样限制有效值,你也必须记录下来,用户必须了解它,并永远记住这个例外。4
在这里,一个私有的、_只在内部使用的_哨兵对象会有帮助。
1
2
3
4
5
6
7
8
9
`_missing = object()
def next(iterator, default=_missing): try: return iterator.next() except StopIteration: if default is _missing: raise return default`
示例输出。
`>>> it = iter([1])
print(next(it, None)) 1 print(next(it, None)) None print(next(it)) Traceback (most recent call last): ... StopIteration`
现在,next()知道default=_missing 意味着_引发异常_,而default=None 只是一个普通的默认值,将被返回。
你可以把__missing_看作是_另一个None_,当实际的None已经被占用时--一个 "高阶 "None。 因为它是模块的私有对象,用户永远不会(意外地)使用它作为默认值,也不会知道它。
关于哨兵对象和相关模式的更深入的解释,请参阅Brandon Rhodes的The Sentinel Object Pattern。
真实世界的例子 #
真正的next()实际上并没有使用sentinel值,因为它是在C语言中实现的,而那里的情况有时是不同的。
但在纯Python代码中,有很多例子。
-
dataclasses模块有两个。
文档中甚至解释了什么是哨兵。
[...]_MISSING_值是一个哨兵对象,用于检测是否提供了_default_和_default_factory_参数。使用这个哨兵对象是因为_None_是_default_的一个有效值。任何代码都不应该直接使用_MISSING_值。
(另一个在生成类的___init___中使用,以显示默认值来自一个工厂)。
-
Werkzeug有一个。
-
我在我的 feed 阅读器库中也有一个(最初是从 Werkzeug 那里偷来的)。我把它用于像
get_feed(feed[, default])这样的方法,它要么引发 FeedNotFoundError 要么返回_default_。
非私有的哨兵 #
我之前提到过哨兵是私有的;但这并不总是如此。
如果哨兵是一个公共方法或函数的默认参数,公开/记录它可能是一个好主意,以方便继承和函数封装。5attrs是一个很好的例子。
如果你不公开它,人们仍然可以通过使用_他们自己的_哨兵来扩展你的代码,然后调用你的函数的任一形式)。
这和打字有什么关系? #
让我们试着在我们的手卷Next()中加入类型提示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
``from typing import overload, TypeVar, Union, Iterator
T = TypeVar('T') U = TypeVar('U')
We define MissingType in one of two ways:
class MissingType: pass
MissingType = object
The second one is equivalent to the original
_missing = object(), but the alias allows us
to keep the same type annotations.
_missing = MissingType()
As mentioned before, next() is actually two functions;
typing.overload allows us to express this.
One that returns an item or raises an exception:
@overload def next(iterator: Iterator[T]) -> T: ...
... and one that takes a default value (of some type U),
and returns either an item, or that default value
(of the same type U):
@overload def next(iterator: Iterator[T], default: U) -> Union[T, U]: ...
The implementation takes all the arguments,
and returns a union of all the types:
def next( iterator: Iterator[T], default: Union[MissingType, U] = _missing ) -> Union[T, U]: try: return iterator.next() except StopIteration:
# "if default is _missing" is idiomatic here,
# but Mypy doesn't understand it
# ("var is None" is a special case).
# It does understand isinstance(), though:
# https://mypy.readthedocs.io/en/stable/casts.html#casts
if isinstance(default, MissingType):
# If MissingType is `object`, this is always true,
# since all types are a subclass of `object`.
raise
return default``
isinstance() ,这就是为什么普通的object() 哨兵不工作的原因--你不能(很容易)让Mypy像对待None这样的内置常量那样对待你自己的 "常量",而且哨兵也没有一个独特的_类型。_
另外,如果你使用MissingType = object 版本,Mypy会抱怨。
next.py:37: error: Overloaded function implementation cannot produce return type of signature 2
如果你想知道好的版本是否真的有效,这里是Mypy的说法。
59
60
61
62
63
64
65
66
67
`it = iter([1, 2])
one = next(it) reveal_type(one)
next.py:62: note: Revealed type is 'builtins.int*'
two = next(it, 'a string') reveal_type(two)
next.py:66: note: Revealed type is 'Union[builtins.int*, builtins.str*]'`
PEP661是怎么回事? #
现在有很多sentinel的实现;仅在标准库中就有_15种不同的实现_。
他们中的许多人至少有其中的一个问题。
- 非描述性的/太长的repr()(例如:
<object object at 0x7f99a355fc20>) - 不能正确地进行pickle(例如,在取消pickle之后,你会得到一个不同的新对象
- 不能很好地使用打字法
因此,PEP 661"建议增加一个定义哨兵值的工具,在stdlib中使用,并作为stdlib的一部分公开提供"。 它看起来像这样。
`>>> NotGiven = sentinel('NotGiven')
NotGiven
MISSING = sentinel('MISSING', repr='mymodule.MISSING') MISSING
mymodule.MISSING`
这个工具将解决所有已知的问题,使开发者(主要是stdlib和第三方库的作者)不用再重新发明轮子(再次)。
这对我有什么影响? #
一点也不。
如果PEP被接受并实施,你将能够用一行代码创建一个没有问题的哨兵。
当然,如果你愿意,你可以继续使用你自己的哨兵对象;PEP甚至没有提议改变标准库中_现有的_哨兵。
这值得做一个PEP吗? #
PEP的存在是为了支持在 "正确 "方式不明显、需要达成共识或协调、或者变化有很大爆炸半径的情况下的讨论。 很多PEP被放弃或拒绝(这很好,这就是这个过程应该做的)。
PEP661似乎属于 "需要达成共识 "的类别;它是在一次社区民意调查之后进行的,尽管投票者的首选是 "什么都不做",但大多数投票者还是选择了 "做一些事情"(但对应该做什么没有_明确的_共识)。
投票介绍中说。
这是一个小细节,所以我认为最重要的是我们要迅速达成一个合理的决定,即使这个决定是什么也不做。
值得记住的是,_什么都不_做永远是一种选择。)
如果你对这种事情感兴趣,我强烈建议你去看看投票线程和(正在进行的)PEP讨论线程--通常,这些讨论是API设计大师的课程。
这就是我现在的全部内容。
如果你学到了新的东西,请考虑分享,这真的很有帮助!:)
如果你想要更多类似的东西,你可以查看我以前的文章,或者通过电子邮件或Atom feed获得未来文章的更新。