Search code examples
pythonpython-decorators

Nested decorators in Python defining simulation methods


Let's say we have three simulation methods:

def method1(func):
    def wrapper(*args, **kwargs):
        #Implementation of some simulator backend
        #but as a toy model we just pass a string here
        return func(*args, simulation_method='method1', **kwargs)
    return wrapper


def method2(func):
    def wrapper(*args, **kwargs):
        #Implementation of some simulator backend
        #but as a toy model we just pass a string here
        return func(*args, simulation_method='method2', **kwargs)
    return wrapper


def method3(func):
    def wrapper(*args, **kwargs):
        #Implementation of some simulator backend
        #but as a toy model we just pass a string here
        return func(*args, simulation_method='method3', **kwargs)
    return wrapper

such that we can call a simulation function with a specific method via

@method3
def simulation(simulation_method):
    #Implementation of some computation that needs to be simulated
    #but as a toy model we just print the following statement:
    print(f"Running simulation with {simulation_method} method")

which yields the output

"Running simulation with method3 method"

I now want to define a decorator called MultiSimulation that repeatedly calls the simulation function while using the given simulation methods with the following syntax:

@MultiSimulation
@method1
@method2
@method3
def simulation(simulation_method):
    print(f"Running simulation with {simulation_method} method")

This should give the output:

"Running simulation with method1 method"
"Running simulation with method2 method"
"Running simulation with method3 method"

I am stuck with the definition of MultiSimulation and would be glad to get some help here. Thanks!

I tried different variations such as

def MultiSimulation(func):
    def repeated_simulation(*args, **kwargs):
        simulation_methods = []
        if hasattr(func, '__wrapped__'):
            simulation_methods = func.__wrapped__.simulation_methods
        result = None
        for simulation_method in simulation_methods:
            kwargs['simulation_method'] = simulation_method
            result = func(*args, **kwargs)
        return result
    repeated_simulation.simulation_methods = []
    repeated_simulation.__wrapped__ = func
    return repeated_simulation

but I don't get any output.


Solution

  • Decorator rework required to keep decorator stacking

    With the rework, here what you can get:

    @MultiSimulation
    @method1
    @method2
    @method3
    def simulation(simulation_method):
        print(f"Running simulation with {simulation_method} method")
        return simulation_method
    
    print(simulation())
    # Running simulation with method1 method
    # Running simulation with method2 method
    # Running simulation with method3 method
    # ['method1', 'method2', 'method3']
    

    You need to update your decorators this way:

    def method1(func):
        def wrapper1(*args, simulation_method="method1", **kwargs):
            return func(*args, simulation_method=simulation_method, **kwargs)
    
        return wrapper1
    

    And you need this decorator:

    def MultiSimulation(func):
        def repeated_simulation(*args, **kwargs):
            tmp_fct = func
            results = []
            while tmp_fct:
                try:
                    results.append(tmp_fct(*args, **kwargs))
                except TypeError:
                    pass
                try:
                    tmp_fct = tmp_fct.__closure__[0].cell_contents
                except TypeError:
                    break
            return results
    
        return repeated_simulation
    

    With this rework of decorators, it's possible to use your original style while getting the return values of the different simulation if necessary.

    def method1(func):
        def wrapper1(*args, simulation_method="method1", **kwargs):
            return func(*args, simulation_method=simulation_method, **kwargs)
    
        return wrapper1
    
    
    def method2(func):
        def wrapper2(*args, simulation_method="method2", **kwargs):
            return func(*args, simulation_method=simulation_method, **kwargs)
    
        return wrapper2
    
    
    def method3(func):
        def wrapper3(*args, simulation_method="method3", **kwargs):
            return func(*args, simulation_method=simulation_method, **kwargs)
    
        return wrapper3
    
    
    def MultiSimulation(func):
        def repeated_simulation(*args, **kwargs):
            tmp_fct = func
            results = []
            while tmp_fct:
                try:
                    results.append(tmp_fct(*args, **kwargs))
                except TypeError:
                    pass
                try:
                    tmp_fct = tmp_fct.__closure__[0].cell_contents
                except TypeError:
                    break
            return results
    
        return repeated_simulation
    
    
    @MultiSimulation
    @method1
    @method2
    @method3
    def simulation(simulation_method):
        print(f"Running simulation with {simulation_method} method")
        return simulation_method
    
    
    print(simulation())
    # Running simulation with method1 method
    # Running simulation with method2 method
    # Running simulation with method3 method
    # ['method1', 'method2', 'method3']