Decorated class looses acces to its attributes

97 views Asked by At

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

1

There are 1 answers

7
Martijn Pieters On BEST 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])