from itertools import chain
from weakref import WeakSet
import operator

class Dynamic:
    @classmethod
    def args(self, *args, **kw):
        return lambda f: Dynamic(f, *args, **kw)
        
    def __init__(self, f, *args, **kw):
        self._ismodified = True
        self._cache = None
        
        self.thunk = lambda: None, (), {}
        self.children = WeakSet()  # тут надо немного посидеть с дебагером
        self.parents = set()       # и убедиться что нет утечек памяти
        
        self.update(f, *args, **kw)
    
    def update(self, f, *args, **kw):
        try:
            thunk = f.thunk
            parents = set(f.parents)
        
        except AttributeError:
            thunk = f, args, kw
            parents = set(chain(args, kw.values()))
        
        if self in parents:              # возможно это можно обработать лучше
            parents.discard(self)
            oldself = Dynamic(self)      # fork self
            
            f, args, kw = thunk
            args = list(args)
            
            for i, a in enumerate(args):  # patch thunk
                if a is self:
                    args[i] = oldself
            
            for k, v in kw.items():
                if v is self:
                    kw[k] = oldself
            
            thunk = f, tuple(args), kw
            parents.add(oldself)
        
        for p in parents:
            p.children.add(self)
        
        for p in self.parents:
            p.children.discard(self)
        
        self.thunk = thunk
        self.parents = parents
        self.setmodified()
    
    def setmodified(self):
        if not self._ismodified:
            self._ismodified = True
        
            for c in self.children:
                c.setmodified()
    
    def compose(self, f, d):
        return Dynamic(f, self, d)
    
    def __get__(self, obj, type=None):
        return self
    
    def __set__(self, obj, d):
        self.update(d)
    
    def __call__(self):
        if self._ismodified:
            self._ismodified = False
            f, args, kw = self.thunk
            
            self._cache = f(
                *[d() for d in args],
                **{k: v() for k, v in kw}
                )
        
        return self._cache
    
    def __hash__(self):
        return id(self)
    
    def __repr__(self):
        return 'Dynamic({}:{})'.format(hash(self), repr(self()))
    
    def __str__(self):
        return str(self())
    
    def __add__(self, d):
        return self.compose(operator.add, d)
    
    def __mul__(self, d):
        return self.compose(operator.mul, d)
    
    def __truediv__(self, d):
        return self.compose(operator.div, d)
    
    def __iadd__(self, d):
        self.update(self + d)
        return self
    
    def __imul__(self, d):
        self.update(self * d)
        return self
    
    def __itruediv__(self, d):
        self.update(self / d)
        return self
    # и т.д...
    # это всё конечно можно сгенерировать на этапе создания экземпляра класса,
    # но мне влом.

x = Dynamic(lambda: 2)
a = Dynamic(lambda: 5)
b = Dynamic(lambda: 2)

c = a * b
print('c:', c)

a += x
print('c:', c)

x.update(lambda: 0)
print('c:', c)

@Dynamic.args(c, b)
def d(c, b):
    return c / b

print('d:', d)

class Table:
    q = Dynamic(lambda: 9)
    w = Dynamic(lambda: 8)
    e = q + w

z = Table()
print('z.e', z.e)

z.q = Dynamic(lambda: 0)
print('z.e', z.e)
