12

I have classes that have attributes set with @property decorator. They function as getter and setter using try and except clauses inside them. If attribute is not set, it gets data from database and uses it to instatiate objects from other classes. I tried to keep the example short, but the code used to instantiate attribute objects is a little different with each attribute. What they have in common is the try-except at the beginning.

class SubClass(TopClass):

    @property
    def thing(self):
        try:
            return self._thing
        except AttributeError:
            # We don't have any thing yet
            pass
        thing = get_some_thing_from_db('thing')
        if not thing:
            raise AttributeError()
        self._thing = TheThing(thing)
        return self._thing

    @property
    def another_thing(self):
        try:
            return self._another_thing
        except AttributeError:
            # We don't have things like this yet
            pass
        another_thing = get_some_thing_from_db('another') 
        if not another_thing:
            raise AttributeError()
        self._another_thing = AnotherThing(another_thing)
        return self._another_thing

    ...etc...

    @property
    def one_more_thing(self):
        try:
            return self._one_more_thing
        except AttributeError:
            # We don't have this thing yet
            pass
        one_thing = get_some_thing_from_db('one') 
        if not one_thing:
            raise AttributeError()
        self._one_more_thing = OneThing(one_thing)
        return self._one_more_thing

My question: is this a proper (e.g. pythonic) way of doing stuff? To me it seems a bit awkward to add the try-except-segment on top of everything. On the other hand it keeps the code short. Or is there a better way of defining attributes?

Gemmu
  • 484
  • 3
  • 14

1 Answers1

19

So long as you are using at least Python 3.2, use the functools.lru_cache() decorator.

import functools
class SubClass(TopClass):

    @property
    @functools.lru_cache()
    def thing(self):
        thing = get_some_thing_from_db('thing')
        if not thing:
            raise AttributeError()
        return TheThing(thing)

A quick runnable example:

>>> import functools
>>> class C:
    @property
    @functools.lru_cache()
    def foo(self):
        print("Called foo")
        return 42


>>> c = C()
>>> c.foo
Called foo
42
>>> c.foo
42

If you have a lot of these you can combine the decorators:

>>> def lazy_property(f):
    return property(functools.lru_cache()(f))

>>> class C:
    @lazy_property
    def foo(self):
        print("Called foo")
        return 42


>>> c = C()
>>> c.foo
Called foo
42
>>> c.foo
42

If you are still on an older version of Python there's a fully featured backport of lru_cache on ActiveState although as in this case you're not passing any parameters when you call it you could probably replace it with something much simpler.

@YAmikep asks how to access the cache_info() method of lru_cache. It's a little bit messy, but you can still access it through the property object:

>>> C.foo.fget.cache_info()
CacheInfo(hits=0, misses=1, maxsize=128, currsize=1)
Duncan
  • 92,073
  • 11
  • 122
  • 156
  • That's just awesome. I'm actually still on 2.7, but the ActiveState implementation looks nice and it's good to know it already exists in Python 3.2. As you said, I might write a little lighter version of it. Thank you for this and I will mark this as an answer. – Gemmu Apr 19 '13 at 08:26
  • @Duncan: how can we access cache_info() when using the lazy_property decorator? Since property wraps functools.lru_cache, this is not accessible anymore? ``c.foo.cache_info()`` does not work.. Thanks – Michael Mar 07 '14 at 23:45
  • @YAmikep I updated my answer to show one way to do this. – Duncan Mar 08 '14 at 09:18
  • Ok thanks. One thing I do not well understand: the cache is on the class? It is then shared by all the instances? Is it not possible to have a cache per instance instead? I would like to cache a property on an instance for the lifetime of the instance but if I create several instances and the shared cache is full, then the cached value of my first instances will be deleted right? Thanks – Michael Mar 08 '14 at 15:59
  • 1
    Is `lazy_property` really [lazy](http://en.wikipedia.org/wiki/Lazy_evaluation) and not merely cached? – Tobias Kienzler Mar 10 '15 at 09:57
  • For shortness, `lazy_property = lambda f: property(functools.lru_cache()(f))` would be even shorter ;) – Tobias Kienzler Mar 10 '15 at 09:59