pythonpython-3.xmakefilepython-decorators

Python class mimic makefile dependency


Q: Is there a better way to do this, or the idea itself is wrong

I have a processing class that creates something with multiple construction steps, such that the next function depends on the previous ones.

I want to have dependencies specified like those in a makefile, and when the dependency does not exist, construct it.

I currently use a decorator to achieve it but it feels non pythonic.

Please take a look

from typing import *
from functools import wraps

def step_method(dependency: Optional[dict[str, Callable]] = None):
    if dependency is None:
        dependency = {}

    def decorator(method):
        @wraps(method)
        def wrapper(self, *args, **kwargs):
            for attr, func in dependency.items():
                if not getattr(self, attr):
                    func(self)
            ret = method(self, *args, **kwargs)
            return ret
        return wrapper
    return decorator

class StepClass:
    def __init__(self, base_val:int):
        self.base_val: int = base_val
        self.a = None
        self.b = []
        self.c = None
        self.d = []
    
    @step_method({})
    def gen_a(self):
        self.a = self.base_val * 2
        
    @step_method({'a': gen_a})
    def create_b(self):
        self.b = [self.a] * 3
        
    @step_method({
        'a': gen_a,
        'b': create_b
    })
    def gen_c(self):
        self.c = sum(self.b) * self.a

    @step_method({'c': gen_c})
    def generate_d(self):
        self.d = list(range(self.c))
        
        
sc = StepClass(10)
sc.base_val = 7   # allow changes before generating starts
sc.b = [1, 2, 3]  # allow dependency value injection
sc.generate_d()
print(sc.a, sc.b, sc.c, sc.d, sep='\n')

I also wonder if it's possible to detect the usage of variables automatically and generate them through a prespecified dict of function if they don't exist yet


Solution

  • This is a good use case of properties, with which you can generate values on demand so there's no need to build a dependency tree.

    As a convention the generated values are cached in attributes of names prefixed with an underscore. Since all your getter methods and setter methods are going to access attributes in a uniform way, you can programmatically create a getter and a setter method for each given calculation function when initializing a property subclass.

    class SettableCachedProperty(property):
        def __init__(self, func):
            def fget(instance):
                if (value := getattr(instance, name, None)) is None:
                    setattr(self, name, value := func(instance))
                return value
    
            def fset(instance, value):
                setattr(instance, name, value)
    
            name = '_' + func.__name__
            super().__init__(fget, fset)
    
    class StepClass:
        def __init__(self, base_val):
            self.base_val = base_val
    
        @SettableCachedProperty
        def a(self):
            return self.base_val * 2
    
        @SettableCachedProperty
        def b(self):
            return [self.a] * 3
    
        @SettableCachedProperty
        def c(self):
            return sum(self.b) * self.a
    

    so that (omitting d in your example for brevity):

    sc = StepClass(10)
    sc.base_val = 7   # allow changes before generating starts
    sc.b = [1, 2, 3]  # allow dependency value injection
    print(sc.a, sc.b, sc.c, sep='\n')
    

    outputs:

    14
    [1, 2, 3]
    84
    

    Demo here