Sentinel对象及其应用程序?

AhT*_*ker 3 python idioms sentinel

我知道在python中内置object()函数返回一个sentinel对象.我很好奇它是什么,但主要是它的应用程序.

exh*_*uma 7

Python 中的对象标识和类

你声明“我知道在 python 中内置 object() 返回一个哨兵对象。” 略有偏差,但并非完全错误,所以让我首先解决这个问题,以确保我们在同一页面上:

object()在Python中只是所有类的父类。在 Python 2 中,这在一段时间内是明确的。在 Python 2 中你必须这样写:

class Foo(object):
    ...
Run Code Online (Sandbox Code Playgroud)

获得所谓的“新式对象”。您也可以定义没有该超类的类,但这只是为了向后兼容,对于这个问题并不重要。

如今,在 Python 3 中,object超类是隐式的。所以所有的类都继承自该类。因此,下面的两个类在 Python 3 中是相同的:

class Foo:
    pass

class Foo(object):
    pass
Run Code Online (Sandbox Code Playgroud)

知道了这一点,我们可以稍微改写你最初的陈述:

...内置 object() 返回一个哨兵对象。

则变为:

...内置 object() 返回类“object”的对象实例

所以,写的时候:

my_sentinel = object()
Run Code Online (Sandbox Code Playgroud)

只是在“内存中的某处”创建一个空对象实例。最后一部分很重要,因为默认情况下,内置id()函数和使用... is ..., 的检查依赖于内存地址。例如:

>>> a = object()
>>> b = object()
>>> a is b
False
Run Code Online (Sandbox Code Playgroud)

这为您提供了一种创建对象实例的方法,您可以使用该实例来检查代码中的某种逻辑,否则这是非常困难甚至不可能的。这就是“哨兵”对象的主要用途

示例用例:区分“无”和“无/未初始化/空/...”

有时该值None是变量的有效值,您可能需要检测“空”或类似的值与 之间的差异None

假设您有一个类为昂贵的操作执行延迟加载,其中“None”是有效值。然后你可以这样写:

#: sentinel value for uninitialised values
UNLOADED = object()

class MyLoader:
    def __init__(self, remote_addr):
        self.value = UNLOADED
        self.remote_addr = remote_addr
    def get(self):
        if self.value is UNLOADED:
            self.value = expensive_operation(self.remote_addr)
        return self.value
Run Code Online (Sandbox Code Playgroud)

现在expensive_operation可以返回任何值。即使 None 或任何其他“虚假”值和“缓存”也将正常工作,不会出现意外错误。它还使代码非常可读,因为它向代码块的读者非常清楚地传达了意图。您还可以为额外的“is_loaded”布尔值节省存储空间(尽管可以忽略不计)。

使用布尔值的相同代码:

class MyLoader:
    def __init__(self, remote_addr):
        self.value = None
        self.remote_addr = remote_addr
        self.is_loaded = False  # <- need for an additional variable
    def get(self):
        if not self.is_loaded:
            self.value = expensive_operation(self.remote_addr)
            self.is_loaded = True  # <- source for a bug if this is forgotten
        return self.value
Run Code Online (Sandbox Code Playgroud)

或者,使用“无”作为默认值:

class MyLoader:
    def __init__(self, remote_addr):
        self.value = None  #  <- We'll use this to detect load state
        self.remote_addr = remote_addr
    def get(self):
        if self.value is None:
            self.value = expensive_operation(self.remote_addr)
            # If the above returned "None" we will never "cache" the result
        return self.value
Run Code Online (Sandbox Code Playgroud)

最后的想法

上面的“MyLoader”示例只是哨兵值可以派上用场的示例之一。它们有助于使代码更具可读性和更具表现力。他们还避免了某些类型的错误。

它们在人们想要用来None表示特殊价值的领域特别有用。每当您想到“当 X 是这种情况时,我会将变量设置为None”之类的事情时,可能值得考虑使用哨兵值。None因为您现在为特定上下文赋予了该值特殊的含义。

另一个这样的例子是无限整数具有特殊值。无穷大的概念只存在于浮点数中。但如果您想确保类型安全,您可能需要创建自己的“特殊”值,例如表示无穷大。

使用这样的哨兵值有助于区分多个不同的概念,否则这是不可能的。如果您需要许多不同的“特殊”值并None在任何地方使用,您最终可能会None在另一个概念的上下文中使用一个概念,并最终产生难以调试的意外副作用。想象一个这样的人为函数:

SENTINEL_A = object()
SENTINEL_B = object()

def foobar(a = SENTINEL_A, b = SENTINEL_B):
    if a is SENTINEL_A:
        a = -12
    if b is SENTINEL_B:
        b = a * 2
    print(a+b)
Run Code Online (Sandbox Code Playgroud)

通过使用这样的哨兵,就不可能通过混合变量来意外触发 if 分支。例如,假设您重构代码并在某个地方出错,像这样混合 a 和 b:

SENTINEL_A = object()
SENTINEL_B = object()

def foobar(a = SENTINEL_A, b = SENTINEL_B):
    if b is SENTINEL_A:  # <- bug: using *b* instead of *a*
        a = -12
    if b is SENTINEL_B:
        b = a * 2
    print(a+b)
Run Code Online (Sandbox Code Playgroud)

在这种情况下,第一个if永远不可能为真(当然,除非函数调用不正确)。如果您使用None默认值,则此错误将变得更难以检测,因为您最终会遇到a = -12意想不到的情况。

从这个意义上说,哨兵使您的代码更加健壮。如果您的代码中出现逻辑错误,它们将更容易被发现。

话虽如此,哨兵值非常罕见。我个人发现它们对于避免过度使用None标记特殊情况非常有用。


Aar*_*ier 6

这是来自 Python 标准库的源代码示例,用于使用哨兵值的数据类

# A sentinel object to detect if a parameter is supplied or not.  Use
# a class to give it a better repr.
class _MISSING_TYPE:
    pass
MISSING = _MISSING_TYPE()
Run Code Online (Sandbox Code Playgroud)


Tre*_*eld 5

object是所有其他类在python 3中继承的基类.对于普通的旧对象,你可以做很多事情.然而,对象的 身份可能是有用的.例如,iter函数接受一个sentinel参数,该参数指示何时停止终止.我们可以提供一个object().

sentinel = object()

def step():
    inp = input('enter something: ')
    if inp == 'stop' or inp == 'exit' or inp == 'done':
        return sentinel
    return inp

for inp in iter(step, sentinel):
    print('you entered', inp)
Run Code Online (Sandbox Code Playgroud)

这将要求输入,直到用户键入停止,退出或完成.我不确定iter哨兵什么时候比发电机更有用,但我想这无论如何都很有意思.

我不确定这是否能回答你的问题.要清楚,这只是一个可能的应用object.从根本上说,它在python语言中的存在与它可用作哨兵价值无关(据我所知).

  • 我已经看到它用作参数的默认值,作为参数的 None 的替代,这些参数可能会接收 None 作为其值,而没有“调用者尚未设置此参数”的通常含义。例如 https://docs.aiohttp.org/en/stable/testing.html#aiohttp.test_utils.make_mocked_request (2认同)