1

I am creating a decorator to cache values of costly functions which can throw exceptions, and I want to record whether we reached the point where the value is initialized or not. As of now, I just initialize the value to an "odd" string new_val = "__THIS_IS_UNINITIALIZED__" but that feels dirty.

I was wondering if using is with a custom class (that doesn't do anything) would be safe.

This is what I have now:

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

And I was wondering if I could use if is Class instead of if new_val == "__THIS_IS_UNINITIALIZED__"

Something like this:

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)

Using the string "__THIS_IS_UNINITIALIZED__" works fine so far (and will work fine for the foreseeable future). This is mostly for learning purposes, to be honest.

Thank you in advance.

Savir
  • 17,568
  • 15
  • 82
  • 136
  • Related, maybe dupe: [Are classobjects singletons?](https://stackoverflow.com/q/33924950/674039) – wim Apr 16 '19 at 00:57
  • Another [possible dupe](https://stackoverflow.com/questions/39313943/sentinel-object-and-its-applications), the source of the update to my answer. – chepner Apr 16 '19 at 14:15

3 Answers3

2

Python doesn't really have "uninitialized variables". A variable doesn't exist until you assign a value to it. If you refer to such a variable before you have assigned a value to it you will get

  1. a NameError if your code refers to a but a is unassigned
  2. an AttributeError if your code does a.b if a exists but b is unassigned
  3. an UnboundLocalError if your code knows about a but it tries to get a value from it before it has been assigned.

(There might be some other cases I have missed.)

You appear to be trying to redesign this arrangement and that is probably not a good idea.

If you want to check that your class instance a has a defined attribute b you could do hasattr(a, "b"). Or simply write a try...except that traps for AttributeError.

But it seems to me you are trying to solve something that is a problem in another language. Referring to an uninitialized variable in languages where such things exist can lead to a segfault. But doing the corresponding thing in Python just results in a runtime error. You don't really need to trap for it because the language does that already.

BoarGules
  • 16,440
  • 2
  • 27
  • 44
2

The standard idiom is to essentially do what you did, but to create an instance of object rather than define a new class for such sentinels.

 Uninitialized = object()

Since your class statement is equivalent to

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

the only difference is that I instantiated object instead of type.


Update (via 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>'

Defining a new class lets you use the class name as a short diagnostic message or description of the purpose of the sentinel.

chepner
  • 497,756
  • 71
  • 530
  • 681
1

I think there are two variants of your pattern that could be considered an improvement.


Sentinels

This is a pretty common pattern, you create something very similar to your unique string called a sentinel value. The cpython library uses is as well, for example in the dataclasses module. You essentially just create and share an empty object that is described as meaning nothing and use it to instantiate variables that have no meaningful value yet.

In this case, you don't need not check with isinstance and can just use is, which is faster, safer, and more idiomatic. Since you use the sentinel's reference id as an identifier, there is no way to accidentally match it.

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()

This is what I'd personally suggest to do in case you want to roll this kind of caching mechanism.


Function Attribute Hacks

Since functions are first class objects, they can have attributes, like any other object. So something like this is completely valid python code:

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

From a stylistic point of view, this is quite horrid though.

Arne
  • 17,706
  • 5
  • 83
  • 99