2

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

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

2 Comments

This is way cleaner than the original code! thanks alot
You're welcome. And thanks for the attempt to improve the answer, but there's no need for a dedicated WeakKeyDictionary as a cache because values are cached as instance attributes, which are instance-specific already. I did update the answer to rename self of the nested functions to avoid confusion with that of the outer function.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.