Python哨兵对象、类型提示和PEP 661

422 阅读9分钟

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___中使用,以显示默认值来自一个工厂)。

  • attrs也有两个,其中一个 (类似于 dataclasses.MISSING) 甚至包括在API 文档中。

  • 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获得未来文章的更新。


  1. 截至2021年6月10日,PEP仍处于草案状态。 [返回]

  2. 这里有一篇2008年关于它的文章[返回]

  3. 虽然 Python不支持重载,但有时以这种方式思考函数是很有用的。 [返回]

  4. 这同样适用于使用其他一些 "普通 "的值,例如,"<NotGiven>" 字符串哨兵。

    对于像字符串这样的不可变的值,情况可能更糟。 因为像interning这样的优化,在不同时间构建的字符串实际上可能导致相同的对象。数据模型 _特别_允许这种情况发生(强调是我的)。

    类型几乎影响了对象行为的所有方面。甚至对象身份的重要性在某种意义上也受到影响:对于不可变的类型,计算新值的操作实际上可以返回对任何具有相同类型和值的现有对象的引用

    ,而对于可变的对象,这是不允许的。 [返回]

  5. 感谢u/energybased提醒我注意这一点! [返回]