Lazy data-flow (spreadsheet like) properties with dependencies in Python

前端 未结 3 2036
孤城傲影
孤城傲影 2020-12-29 10:00

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

3条回答
  •  心在旅途
    2020-12-29 10:12

    import collections
    
    sentinel=object()
    
    class ManagedProperty(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 is None:
                # Allows getattr(cls,mprop) to return the ManagedProperty instance
                return self
            try:
                return getattr(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 @register
            map(obj.__delattr__,getattr(obj,'_dependencies').get(self.property_name,tuple()))
            setattr(obj,self.private_name,value)        
        def __delete__(self,obj):
            if hasattr(obj,self.private_name):
                delattr(obj,self.private_name)
    
    def register(*mproperties):
        def flatten_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 is None:
                all_deps = set()
            for dep in deptree.get(name,tuple()):
                all_deps.add(dep)
                flatten_dependencies(dep, deptree, all_deps)
            return all_deps
    
        def classdecorator(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 in dir(cls):
                mprop=getattr(cls,propname)
                if not isinstance(mprop,ManagedProperty):
                    continue
                for 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
        def count(meth):
            def wrapper(self,*args):
                countname=meth.func_name+'_count'
                setattr(self,countname,getattr(self,countname,0)+1)
                return meth(self,*args)
            return wrapper
    
        class Test(unittest.TestCase):
            def setUp(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')))
                class Foo(object):
                    @count
                    def calc_a(self):
                        return self.b + self.c
                    @count
                    def calc_c(self):
                        return self.d * 2
                @register(ManagedProperty('c',calculate='calc_c',depends_on=('b',)),
                          ManagedProperty('a',calculate='calc_a',depends_on=('b','c')))
                class Bar(Foo):
                    @count
                    def calc_c(self):
                        return self.b * 3
                self.Foo=Foo
                self.Bar=Bar
                self.foo=Foo()
                self.foo2=Foo()            
                self.bar=Bar()
    
            def test_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)
    
    
            def test_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)
    
            def test_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'])})
    
            def test_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)
    

提交回复
热议问题