Skip to content Skip to sidebar Skip to footer

Lazy Data-flow (spreadsheet Like) Properties With Dependencies In Python

My problem is the following: I have some python classes that have properties that are derived from other properties; and those should be cached once they are calculated, and the ca

Solution 1:

Here, this should do the trick. The descriptor mechanism (through which the language implements "property") is more than enough for what you want.

If the code bellow does not work in some corner cases, just write me.

classDependentProperty(object):
    def__init__(self, calculate=None, default=None, depends_on=()):
        # "name" and "dependence_tree" properties are attributes# set up by the metaclass of the owner classif calculate:
            self.calculate = calculate
        else:
            self.default = default
        self.depends_on = set(depends_on)

    def__get__(self, instance, owner):
        ifhasattr(self, "default"):
            return self.default
        ifnothasattr(instance, "_" + self.name):
            setattr(instance, "_" + self.name,
                self.calculate(instance, getattr(instance, "_" + self.name + "_last_value")))
        returngetattr(instance, "_" + self.name)

    def__set__(self, instance, value):
        setattr(instance, "_" + self.name + "_last_value", value)
        setattr(instance, "_" + self.name, self.calculate(instance, value))
        for attr in self.dependence_tree[self.name]:
            delattr(instance, attr)

    def__delete__(self, instance):
        try:
            delattr(instance, "_" + self.name)
        except AttributeError:
            passdefassemble_tree(name,  dict_, all_deps = None):
    if all_deps isNone:
        all_deps = set()
    for dependance in dict_[name].depends_on:
        all_deps.add(dependance)
        assemble_tree(dependance, dict_, all_deps)
    return all_deps

definvert_tree(tree):
    new_tree = {}
    for key, val in tree.items():
        for dependence in val:
            if dependence notin new_tree:
                new_tree[dependence] = set()
            new_tree[dependence].add(key)
    return new_tree

classDependenceMeta(type):
    def__new__(cls, name, bases, dict_):
        dependence_tree = {}
        properties = []
        for key, val in dict_.items():
            ifnotisinstance(val, DependentProperty):
                continue
            val.name = key
            val.dependence_tree = dependence_tree
            dependence_tree[key] = set()
            properties.append(val)
        inverted_tree = {}
        forpropertyin properties:
            inverted_tree[property.name] = assemble_tree(property.name, dict_)
        dependence_tree.update(invert_tree(inverted_tree))
        returntype.__new__(cls, name, bases, dict_)


if __name__ == "__main__":
    # Example and visual test:classBla:
        __metaclass__ = DependenceMeta

        defcalc_b(self, x):
            print"Calculating b"return x + self.a

        defcalc_c(self, x):
            print"Calculating c"return x + self.b

        a = DependentProperty(default=10)    
        b = DependentProperty(depends_on=("a",), calculate=calc_b)
        c = DependentProperty(depends_on=("b",), calculate=calc_c)




    bla = Bla()
    bla.b = 5
    bla.c = 10print bla.a, bla.b, bla.c
    bla.b = 10print bla.b
    print bla.c

Solution 2:

I would like to have something like Makefile rules

then use one! You may consider this model:

  • one rule = one python file
  • one result = one *.data file
  • the pipe is implemented as a makefile or with another dependency analysis tool (cmake, scons)

The hardware test team in our company use such a framework for intensive exploratory tests:

  • you can integrate other languages and tools easily
  • you get a stable and proven solution
  • computations may be distributed one multiple cpu/computers
  • you track dependencies on values and rules
  • debug of intermediate values is easy

the (big) downside to this method is that you have to give up python import keyword because it creates an implicit (and untracked) dependency (there are workarounds for this).

Solution 3:

import collections

sentinel=object()

classManagedProperty(object):
    '''
    If deptree = {'a':set('b','c')}, then ManagedProperties `b` and
    `c` will be reset whenever `a` is modified.
    '''def__init__(self,property_name,calculate=None,depends_on=tuple(),
                 default=sentinel):
        self.property_name=property_name
        self.private_name='_'+property_name 
        self.calculate=calculate
        self.depends_on=depends_on
        self.default=default
    def__get__(self,obj,objtype):
        if obj isNone:
            # Allows getattr(cls,mprop) to return the ManagedProperty instancereturn self
        try:
            returngetattr(obj,self.private_name)
        except AttributeError:
            result=(getattr(obj,self.calculate)()
                    if self.default is sentinel else self.default)
            setattr(obj,self.private_name,result)
            return result
    def__set__(self,obj,value):
        # obj._dependencies is defined by @registermap(obj.__delattr__,getattr(obj,'_dependencies').get(self.property_name,tuple()))
        setattr(obj,self.private_name,value)        
    def__delete__(self,obj):
        ifhasattr(obj,self.private_name):
            delattr(obj,self.private_name)

defregister(*mproperties):
    defflatten_dependencies(name, deptree, all_deps=None):
        '''
        A deptree such as {'c': set(['a']), 'd': set(['c'])} means
        'a' depends on 'c' and 'c' depends on 'd'.

        Given such a deptree, flatten_dependencies('d', deptree) returns the set
        of all property_names that depend on 'd' (i.e. set(['a','c']) in the
        above case).
        '''if all_deps isNone:
            all_deps = set()
        for dep in deptree.get(name,tuple()):
            all_deps.add(dep)
            flatten_dependencies(dep, deptree, all_deps)
        return all_deps

    defclassdecorator(cls):
        deptree=collections.defaultdict(set)
        for mprop in mproperties:
            setattr(cls,mprop.property_name,mprop)
        # Find all ManagedProperties in dir(cls). Note that some of these may be# inherited from bases of cls; they may not be listed in mproperties.# Doing it this way allows ManagedProperties to be overridden by subclasses.for propname indir(cls):
            mprop=getattr(cls,propname)
            ifnotisinstance(mprop,ManagedProperty):
                continuefor underlying_prop in mprop.depends_on:
                deptree[underlying_prop].add(mprop.property_name)

        # Flatten the dependency tree so no recursion is necessary. If one were# to use recursion instead, then a naive algorithm would make duplicate# calls to __delete__. By flattening the tree, there are no duplicate# calls to __delete__.
        dependencies={key:flatten_dependencies(key,deptree)
                      for key in deptree.keys()}
        setattr(cls,'_dependencies',dependencies)
        return cls
    return classdecorator

These are the unit tests I used to verify its behavior.

if __name__ == "__main__":
    import unittest
    import sys
    defcount(meth):
        defwrapper(self,*args):
            countname=meth.func_name+'_count'setattr(self,countname,getattr(self,countname,0)+1)
            return meth(self,*args)
        return wrapper

    classTest(unittest.TestCase):
        defsetUp(self):
            @register(
                ManagedProperty('d',default=0),
                ManagedProperty('b',default=0),
                ManagedProperty('c',calculate='calc_c',depends_on=('d',)),
                ManagedProperty('a',calculate='calc_a',depends_on=('b','c')))classFoo(object):
                @countdefcalc_a(self):
                    return self.b + self.c
                @countdefcalc_c(self):
                    return self.d * 2            @register(ManagedProperty('c',calculate='calc_c',depends_on=('b',)),
                      ManagedProperty('a',calculate='calc_a',depends_on=('b','c')))classBar(Foo):
                @countdefcalc_c(self):
                    return self.b * 3
            self.Foo=Foo
            self.Bar=Bar
            self.foo=Foo()
            self.foo2=Foo()            
            self.bar=Bar()

        deftest_two_instances(self):
            self.foo.b = 1
            self.assertEqual(self.foo.a,1)
            self.assertEqual(self.foo.b,1)
            self.assertEqual(self.foo.c,0)
            self.assertEqual(self.foo.d,0)

            self.assertEqual(self.foo2.a,0)
            self.assertEqual(self.foo2.b,0)
            self.assertEqual(self.foo2.c,0)
            self.assertEqual(self.foo2.d,0)


        deftest_initialization(self):
            self.assertEqual(self.foo.a,0)
            self.assertEqual(self.foo.calc_a_count,1)
            self.assertEqual(self.foo.a,0)
            self.assertEqual(self.foo.calc_a_count,1)            
            self.assertEqual(self.foo.b,0)
            self.assertEqual(self.foo.c,0)
            self.assertEqual(self.foo.d,0)
            self.assertEqual(self.bar.a,0)
            self.assertEqual(self.bar.b,0)
            self.assertEqual(self.bar.c,0)
            self.assertEqual(self.bar.d,0)

        deftest_dependence(self):
            self.assertEqual(self.Foo._dependencies,
                             {'c': set(['a']), 'b': set(['a']), 'd': set(['a', 'c'])})

            self.assertEqual(self.Bar._dependencies,
                             {'c': set(['a']), 'b': set(['a', 'c'])})

        deftest_setting_property_updates_dependent(self):
            self.assertEqual(self.foo.a,0)
            self.assertEqual(self.foo.calc_a_count,1)

            self.foo.b = 1# invalidates the calculated value stored in foo.a
            self.assertEqual(self.foo.a,1)
            self.assertEqual(self.foo.calc_a_count,2)
            self.assertEqual(self.foo.b,1)
            self.assertEqual(self.foo.c,0)
            self.assertEqual(self.foo.d,0)

            self.foo.d = 2# invalidates the calculated values stored in foo.a and foo.c
            self.assertEqual(self.foo.a,5)
            self.assertEqual(self.foo.calc_a_count,3)
            self.assertEqual(self.foo.b,1)
            self.assertEqual(self.foo.c,4)
            self.assertEqual(self.foo.d,2)

            self.assertEqual(self.bar.a,0)
            self.assertEqual(self.bar.calc_a_count,1)
            self.assertEqual(self.bar.b,0)
            self.assertEqual(self.bar.c,0)
            self.assertEqual(self.bar.calc_c_count,1)
            self.assertEqual(self.bar.d,0)

            self.bar.b = 2
            self.assertEqual(self.bar.a,8)
            self.assertEqual(self.bar.calc_a_count,2)
            self.assertEqual(self.bar.b,2)
            self.assertEqual(self.bar.c,6)
            self.assertEqual(self.bar.calc_c_count,2)
            self.assertEqual(self.bar.d,0)

            self.bar.d = 2
            self.assertEqual(self.bar.a,8)
            self.assertEqual(self.bar.calc_a_count,2)            
            self.assertEqual(self.bar.b,2)
            self.assertEqual(self.bar.c,6)
            self.assertEqual(self.bar.calc_c_count,2)
            self.assertEqual(self.bar.d,2)

    sys.argv.insert(1,'--verbose')
    unittest.main(argv=sys.argv)

Post a Comment for "Lazy Data-flow (spreadsheet Like) Properties With Dependencies In Python"