Python 中的对象标识和类
你声明 “我知道在 python 中内置 object() 返回一个哨兵对象。” 有点不对劲,但并非完全错误,所以让我先解决这个问题,以确保我们是在同一页面上:
object() 在 Python 中只是所有类的父类。在 Python 2 中,这有一段时间是明确的。在 Python 2 中,您必须编写:
class Foo(object):
...
得到一个所谓的“新式对象”。你也可以定义没有那个超类的类,但这只是为了向后兼容,对于这个问题并不重要。
今天在 Python 3 中,object 超类是隐式的。所以所有类都继承自该类。因此,以下两个类在 Python 3 中是相同的:
class Foo:
pass
class Foo(object):
pass
知道了这一点,我们可以稍微改一下你最初的陈述:
... 内置 object() 返回一个哨兵对象。
然后变成:
... 内置 object() 返回类“object”的对象实例
所以,写的时候:
my_sentinel = object()
只需在“内存某处”创建一个空对象实例。最后一部分很重要,因为默认情况下,内置的id() 函数和使用... is ... 的检查依赖于内存地址。例如:
>>> a = object()
>>> b = object()
>>> a is b
False
这为您提供了一种创建对象实例的方法,您可以使用它来检查代码中的某种逻辑,否则这些逻辑非常困难甚至是不可能的。 这是“哨兵”对象的主要用途。
示例用例:区分“None”和“Nothing/Uninitialised/Empty/...”
有时值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
现在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
或者,使用“无”作为默认值:
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
最后的想法
上面的“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)
通过使用这样的哨兵,不可能通过混合变量意外触发 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)
在这种情况下,第一个 if 永远不会为真(当然,除非函数调用不正确)。如果您使用 None 作为默认值,则此错误将变得更难检测,因为您最终会得到a = -12,以防万一。
从这个意义上说,哨兵使您的代码更加健壮。如果您的代码中出现逻辑错误,它们将更容易找到。
话虽如此,哨兵值相当罕见。我个人认为它们对于避免过度使用 None 来标记特殊情况非常有用。