Decorated class looses acces to its attributes
Question: I implemented a decorator that worked like a charm until I added attributes to
the decorated class. When I instantiate the class, it cannot acces the calss
attributes. Take the following minimal working example :

    
    
    from module import specialfunction
    
    class NumericalMathFunctionDecorator:
        def __init__(self, enableCache=True):
            self.enableCache = enableCache
    
        def __call__(self, wrapper):
            def numericalmathfunction(*args, **kwargs):
                func = specialfunction(wrapper(*args, **kwargs))
                """
                Do some setup to func with decorator arguments (e.g. enableCache)
                """
            return numericalmathfunction
    
    @NumericalMathFunctionDecorator(enableCache=True)
    class Wrapper:
        places = ['home', 'office']
        configs = {
                   'home':
                      {
                       'attr1': 'path/at/home',
                       'attr2': 'jhdlt'
                      },
                   'office':
                      {
                       'attr1': 'path/at/office',
                       'attr2': 'sfgqs'
                      }
                  }
        def __init__(self, where='home'):
            # Look for setup configuration on 'Wrapper.configs[where]'.
            assert where in Wrapper.places, "Only valid places are {}".format(Wrapper.places)
            self.__dict__.update(Wrapper.configs[where])
    
        def __call__(self, X):
            """Do stuff with X and return the result
            """
            return X ** 2
    
    model = Wrapper()
    

When I instantiate the Wrapper class (#1), I get the following error :

    
    
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-5-a99bd3d544a3> in <module>()
         15         assert where in Wrapper.places, "Only valid places are {}".format(Wrapper.places)
         16 
    ---> 17 model = Wrapper()
    
    <ipython-input-5-a99bd3d544a3> in numericalmathfunction(*args, **kwargs)
          5     def __call__(self, wrapper):
          6         def numericalmathfunction(*args, **kwargs):
    ----> 7             func = wrapper(*args, **kwargs)
          8         return numericalmathfunction
          9 
    
    <ipython-input-5-a99bd3d544a3> in __init__(self, where)
         13     def __init__(self, where='home'):
         14         # Look for setup configuration on 'Wrapper.configs[where]'.
    ---> 15         assert where in Wrapper.places, "Only valid places are {}".format(Wrapper.places)
         16 
         17 model = Wrapper()
    
    AttributeError: 'function' object has no attribute 'places'
    

I guess that with the decorator, Wrapper becomes a function that looses acces
to its attributes...

Any ideas of how I can solve this ? Maybe there is a workaround


Answer: You replaced `Wrapper` (which was a class) with the `numericalmathfunction`
function object. That object doesn't have any of the class attributes, no.

In essence, the decorator does this:

    
    
    class Wrapper:
        # ...
    
    Wrapper = NumericalMathFunctionDecorator(enableCache=True)(Wrapper)
    

so whatever the `NumericalMathFunctionDecorator.__call__` method returns has
now replaced the class; all references to `Wrapper` now reference that return
value. And when you use the name `Wrapper` in the `__init__` method, you are
referencing that global, not the original class.

You can still access the current class with `type(self)`, or just reference
those attributes via `self` (where the name lookup falls through to the
class):

    
    
    def __init__(self, where='home'):
        # Look for setup configuration on 'Wrapper.configs[where]'.
        assert where in self.places, "Only valid places are {}".format(self.places)
        self.__dict__.update(self.configs[where])
    

or

    
    
    def __init__(self, where='home'):
        # Look for setup configuration on 'Wrapper.configs[where]'.
        cls = type(self)
        assert where in cls.places, "Only valid places are {}".format(cls.places)
        self.__dict__.update(cls.configs[where])
    

In both cases you can end up with referencing an attribute on a subclass if
you ever did subclass `Wrapper` (which you cannot do in this case anyway as
you would have to fish the class out of the decorator closure).

Alternatively, you could store the original class as an attribute on the
returned function:

    
    
    def __call__(self, wrapper):
        def numericalmathfunction(*args, **kwargs):
            func = specialfunction(wrapper(*args, **kwargs))
            """
            Do some setup to func with decorator arguments (e.g. enableCache)
            """
        numericalmathfunction.__wrapped__ = wrapper
        return numericalmathfunction
    

then use that reference in your `__init__`:

    
    
    def __init__(self, where='home'):
        # Look for setup configuration on 'Wrapper.configs[where]'.
        cls = Wrapper
        while hasattr(cls, '__wrapped__'):
            # remove any decorator layers to get to the original
            cls = cls.__wrapped__
        assert where in cls.places, "Only valid places are {}".format(cls.places)
        self.__dict__.update(cls.configs[where])
    

