使用 is (CustomClass) 可以安全地检测 Python 中的未初始化值

Is using `is (CustomClass)` safe to detect uninitialized values in Python

我正在创建一个装饰器来缓存可能抛出异常的昂贵函数的值,我想记录我们是否到达了初始化值的位置。截至目前,我只是将值初始化为 "odd" 字符串 new_val = "__THIS_IS_UNINITIALIZED__" 但是 感觉很脏 .

我想知道将 is 与自定义 class(什么都不做)一起使用是否安全。

这是我现在拥有的:

class Cache:
    _cached = {}
    @staticmethod
    def cache(prefix):
        def wrapper(wrapped):
            def inner(self, *args, **kwargs):
                new_val = "__THIS_IS_UNINITIALIZED__"
                key = Cache.make_key(*args, **kwargs)
                if key in Cache._cached:
                    print("cache hit")
                    return Cache._cached[key]
                print("cache miss")
                try:
                    # This can throw exceptions
                    new_val = wrapped(self, *args, **kwargs)
                    # Something below this can ALSO throw
                    # exceptions, but the value has been
                    # initialized at this point.
                except:
                    if new_val == "__THIS_IS_UNINITIALIZED__":
                        print("we never got to initialize :_( ")
                else:
                    Cache._cache[key] = new_val
            return inner
        return wrapper

我想知道我是否可以使用 if is Class 而不是 if new_val == "__THIS_IS_UNINITIALIZED__"

像这样:

class Uninitialized:
    pass

class Cache:
    _cached = {}
    @staticmethod
    def cache(prefix):
        def wrapper(wrapped):
            def inner(self, *args, **kwargs):
                new_val = Uninitialized
                key = Cache.make_key(*args, **kwargs)
                if key in Cache._cached:
                    print("cache hit")
                    return Cache._cached[key]
                print("cache miss")
                try:
                    # This can throw exceptions
                    new_val = wrapped(self, *args, **kwargs)
                    # Something below this can ALSO throw
                    # exceptions, but the value has been
                    # initialized at this point.
                except:
                    if new_val is Uninitialized:
                        print("we never got to initialize :_( ")
                else:
                    Cache._cache[key] = new_val
            return inner
        return wrapper

    @staticmethod
    def make_key(*args, **kwargs):
        positional = ':'.join([str(s) for s in args])
        kw = ':'.join('%s=%s' % kf for kv in kwargs.items())
        return ':'.join([positional, kw])

class Test:
    def __init__(self):
        self.foo = 'hi'

    @Cache.cache('api')
    def test_cache(self, a, b):
        raise Exception("foo")

if __name__ == "__main__":
    t = Test()
    t.test_cache(1, 2)

使用字符串 "__THIS_IS_UNINITIALIZED__" 到目前为止工作正常(并且在可预见的将来也将工作正常)。老实说,这主要是出于学习目的。

提前致谢。

Python 并没有真正的 "uninitialized variables"。变量不存在,直到你给它赋值。如果在为它赋值之前引用这样的变量,你将得到

  1. a NameError 如果您的代码引用 aa 未分配
  2. 一个AttributeError如果你的代码a.b如果a存在但b未分配
  3. 一个 UnboundLocalError 如果你的代码知道 a 但它试图在它被分配之前从它获取一个值。

(可能还有一些我遗漏的案例。)

您似乎正在尝试重新设计这种安排,这可能不是一个好主意。

如果您想检查 class 实例 a 是否具有已定义的属性 b,您可以 hasattr(a, "b")。或者简单地写一个 try...except 来捕获 AttributeError

但在我看来,您正在尝试解决另一种语言中存在的问题。在存在此类事物的语言中引用未初始化的变量可能会导致段错误。但是在 Python 中做相应的事情只会导致运行时错误。你真的不需要为它设置陷阱,因为语言已经这样做了。

标准的习惯用法是基本上做你所做的,但是创建一个 object 的实例而不是为这样的哨兵定义一个新的 class。

 Uninitialized = object()

由于您的 class 语句等同于

Uninitialized = type("Uninitialized", (object,), {})

唯一的区别是我实例化了 object 而不是 type


更新(通过 Sentinel object and its applications?): an instance of a custom class can provide a more useful representation, as demonstrated by an example from the dataclasses module:

>>> from dataclasses import MISSING
>>> repr(MISSING)
'<dataclasses._MISSING_TYPE object at 0x10baeaf98>'
>>> repr(object())
'<object object at 0x10b961200>'

定义一个新的 class 允许您使用 class 名称作为简短的诊断消息或对哨兵用途的描述。

我认为您的模式有两种变体可以被视为改进。


哨兵

这是一个非常常见的模式,您创建了一些与您的独特字符串非常相似的东西,称为标记值。 cpython 库也使用 is,例如在 the dataclasses module 中。您基本上只是创建和共享一个被描述为没有任何意义的空对象,并使用它来实例化尚无有意义值的变量。

在这种情况下,您不需要不检查isinstance,只需使用is,它更快、更安全、更地道。由于您使用哨兵的参考 ID 作为标识符,因此无法意外匹配它。

constants.py

SENTINEL = object()
# some other constants maybe

code.py

from constants import SENTINEL

COSTLY_COMPUTE_RESULT = SENTINEL

def costly_compute():
    global COSTLY_COMPUTE_RESULT 
    if COSTLY_COMPUTE_RESULT is not SENTINEL:
        return COSTLY_COMPUTE_RESULT 
    COSTLY_COMPUTE_RESULT = ...  # assume this is some heavy lifting
    return costly_compute()

如果您想使用这种缓存机制,我个人建议您这样做。


函数属性黑客

由于函数首先是 class 对象,因此它们可以像任何其他对象一样具有属性。所以像这样的东西是完全有效的 python 代码:

def costly_compute():
    try:
        return costly_compute.cache
    except AttributeError:
        pass
    costly_compute.cache = ...  # some heavy lifting again
    return costly_compute()

从文体的角度来看,这很糟糕。