我正在创建一个装饰器来缓存可能抛出异常的代价高昂的函数的值,我想记录我们是否达到了初始化值的点。截至目前,我只是将值初始化为“奇数”字符串new_val = "__THIS_IS_UNINITIALIZED__"
,但感觉很脏。
我想知道是否使用is
与自定义类(没有做任何事情)是安全的。
这就是我现在拥有的:
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__"
到目前为止工作正常(并且在可预见的未来将正常工作)。说实话,这主要是为了学习目的。
先感谢您。
标准习惯用法基本上是你做的,但要创建object
的实例,而不是为这样的标记定义一个新的类。
Uninitialized = object()
因为你的类声明等同于
Uninitialized = type("Uninitialized", (object,), {})
唯一的区别是我实例化了object
而不是type
。
更新(通过Sentinel object and its applications?):自定义类的实例可以提供更有用的表示,如the dataclasses
module中的示例所示:
>>> from dataclasses import MISSING
>>> repr(MISSING)
'<dataclasses._MISSING_TYPE object at 0x10baeaf98>'
>>> repr(object())
'<object object at 0x10b961200>'
通过定义新类,您可以将类名用作短诊断消息或描述目标的描述。
Python实际上没有“未初始化的变量”。在为其赋值之前,变量不存在。如果在为其分配值之前引用这样的变量,您将获得
NameError
,如果你的代码引用a
但a
未分配AttributeError
,如果您的代码执行a.b
,如果a
存在但b
未分配UnboundLocalError
,如果你的代码知道a
但它试图在分配之前从中获取一个值。(可能还有其他一些我错过的案例。)
你似乎试图重新设计这种安排,这可能不是一个好主意。
如果你想检查你的类实例a
是否有定义的属性b
你可以做hasattr(a, "b")
。或者只是写一个try...except
陷阱AttributeError
。
但在我看来,你正试图解决另一种语言中的问题。在存在此类事物的语言中引用未初始化的变量会导致段错误。但是在Python中执行相应的操作只会导致运行时错误。你真的不需要陷阱,因为语言已经这样做了。
我认为你的模式有两种变体可以被认为是一种改进。
这是一种非常常见的模式,您创建的东西与您称为sentinel值的唯一字符串非常相似。 cpython库也使用,例如在the dataclasses module中。您基本上只是创建和共享一个被描述为没有任何意义的空对象,并使用它来实例化尚无有意义值的变量。
在这种情况下,你不需要检查isinstance
,只需使用更快,更安全,更惯用的is
。由于您使用sentinel的引用ID作为标识符,因此无法意外地匹配它。
constants.朋友
SENTINEL = object()
# some other constants maybe
code.朋友
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()
这是我个人建议你做的,以防你想要推出这种缓存机制。
由于函数是第一类对象,因此它们可以具有属性,就像任何其他对象一样。所以像这样的东西是完全有效的python代码:
def costly_compute():
try:
return costly_compute.cache
except AttributeError:
pass
costly_compute.cache = ... # some heavy lifting again
return costly_compute()
从风格的角度来看,这是非常可怕的。