0

I want to use something like the usual lazy property decorator, but due to how TensorFlow works and how I use it, I need all of the lazy properties to be initialized automatically at __init__ the latest (the TensorFlow part is not part of the question, but see here for what I mean). By "initializing" I just mean calling getattr to run the property method once and cache the result.

The following works already:

import functools

def graph_property(getter):
    property_name = getter.__name__
    attribute = '_cache_' + property_name

    @property
    @functools.wraps(getter)
    def decorated(self):
        if not hasattr(self, attribute):
            setattr(self, attribute, getter(self))
            self._graph.append(property_name) # for illustration
            print('Initializing ' + property_name)
        return getattr(self, attribute)

    return decorated


class Test:
    def __init__(self):
        self._graph = []
        self.inputs    # DON'T LIKE TO DO THIS
        self.do_stuff  # AND THIS

    @graph_property
    def inputs(self):
        return 42.0

    @graph_property
    def do_stuff(self):
        return self.inputs + 1.0


if __name__ == '__main__':
    t = Test()
    print(t._graph)

However, it would be nice to get rid of the manual calls to self.input and self.do_stuff in __init__ -- that quickly gets tedious.

I was thinking about multiple ways of "remembering" which properties are graph_propertys somewhere in a list, but all must fail, I think, since at the time the decorator is applied, the class is not yet known to it (let alone self).

One way I could imagine to work is giving the returned decorated object some tag attribute, and write a metaclass for Test which looks at all methods, collects the ones with this tag, and somehow creates an initializer for them. I failed to implement this because I'm very not familiar with metaclasses and the property descriptor doesn't let me add attributes.

Would the described approach be feasible (if so, how)? Or is there an easier way (without manual overhead and with equally nice syntax) and I'm just not seeing it?

Community
  • 1
  • 1
phipsgabler
  • 20,535
  • 4
  • 40
  • 60

2 Answers2

1

You could add a simple mixin and define a subclass of property and then do all of the initializing related to this custom property in the __init__ method of mixin. This way you can choose in which class you want them to initialize and when you don't want them initialized.

import functools


class lazy_property(property):
    """
    This class will help us in identifying our lazy properties, so that we
    don't confuse them with normal properties. 
    """
    pass

def graph_property(getter):
    property_name = getter.__name__
    attribute = '_cache_' + property_name

    @lazy_property
    @functools.wraps(getter)
    def decorated(self):
        if not hasattr(self, attribute):
            setattr(self, attribute, getter(self))
            self._graph.append(property_name)  # for illustration
            print('Initializing ' + property_name)
        return getattr(self, attribute)

    return decorated

class InitializeLazyPropertiesMixin:
    """
    This mixin does all of the work of initializing lazy properties
    """
    def __init__(self):
        cls = type(self)
        fields = (k for k in dir(cls) if isinstance(getattr(cls, k), lazy_property))
        for field in fields:
            getattr(self, field)


class Test(InitializeLazyPropertiesMixin):
    def __init__(self):
        self._graph = []
        # Whenever you're inheriting from this mixin make sure to call
        # super `__init__` method.
        super().__init__()

    @graph_property
    def inputs(self):
        return 42.0

    @graph_property
    def do_stuff(self):
        return self.inputs + 1.0

class Test1:
    """
    Just another class that doesn't require initializing any of the lazy properties
    """
    def __init__(self):
        self._graph = []

    @graph_property
    def inputs(self):
        return 42.0

    @graph_property
    def do_stuff(self):
        return self.inputs + 1.0

Demo output:

>>> t = Test()
Initializing inputs
Initializing do_stuff
>>> print(t._graph)
['inputs', 'do_stuff']
>>> t = Test1()
>>> print(t._graph)
[]
>>> t.inputs
Initializing inputs
42.0
>>> t._graph
['inputs']
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
  • Ah, this subclassing trick is beautiful! I have a common base class anyhow, so this fits perfectly. – phipsgabler Feb 21 '17 at 12:41
  • 1
    It just occurred me that `vars(type(self)).items()` won't include variables defined in super-classes - this just checks the descriptors on the declared class itself, not on it ancestors. – jsbueno Feb 22 '17 at 04:10
  • @jsbueno Good catch, updated to use `dir()` which [goes recursively](http://stackoverflow.com/a/33089239/846892) to get the attributes. – Ashwini Chaudhary Feb 22 '17 at 06:36
0

Since you have full control of your properties and class hierarchy, it is simply a matter of marking the properties you want initalized, and have code in a base class __init__ method that will call all of them.

So, first, in your decorator, set a variable on your graph_property decorator, so that it marks the methods to be initialized. Since property objects, unlike functions, can't be assigned arbitrary attributes, the fix for that is to wrap Python's native property in a user-defined class:

class MarcableProperty(property):
    pass

def graph_property(getter):
    property_name = getter.__name__
    attribute = '_cache_' + property_name

    @MarcableProperty
    @functools.wraps(getter)
    def decorated(self):
        ...

    decorated._graph_initialize = True
    return decorated

And then, on a Base or mixin class for all your other classes, do this:

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    for cls_member_name in dir(self.__class__): 
         # "dir" is good because it automatically looks
         # at the superclasses as well
         cls_member = getattr(self.__class__, cls_member_name)
         if getattr(cls_member, "_graph_initialize", False):
              # Fetch property, initializing its value:
              getattr(self, cls_member_name)

And that should be it.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • That was exactly my initial idea, but setting a variable on `decorated` results in "AttributeError: 'property' object has no attribute '_graph_initialize'". – phipsgabler Feb 21 '17 at 12:35
  • That just means that Python's `property` uses `__slots__` - just use a custom property that can be aas simple as : `class MyProperty(property): pass` – jsbueno Feb 21 '17 at 12:57
  • (I've updated the answer with this example) and actually, you dn even need the mark now, just check if the class attribute is an instance of MarcableProperty - just like it is in @Ashwini's answer) – jsbueno Feb 21 '17 at 12:59
  • Yeah, I recognized that would work but be redundant. Thanks anyway! – phipsgabler Feb 21 '17 at 13:01
  • 1
    Yep, [`property` implements](https://github.com/python/cpython/blob/master/Objects/descrobject.c#L1671) something called [`tp_members`](https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_members) slot, which is equivalent to `__slots__`. – Ashwini Chaudhary Feb 21 '17 at 13:04