Search code examples
pythonpython-decorators

python class decorator inside method


I try to use decorator pattern for class hierarchy, so I define class decorator as below:

def delegate(cls, target='_base'):
    class Wrapper(object):
        def __init__(self, *args, **kwargs):
            self.wrapped = cls(*args, **kwargs)

        def __getattr__(self, name):
            if hasattr(self.wrapped, name):
                return getattr(self.wrapped, name)
            else:
                return getattr(self.wrapped.__dict__.get(target), name)

    return Wrapper

class A(object):
    def __init__(self):
      pass

    def foo(self):
      print('this is foo()')


@delegate
class B(object):
    def __init__(self):
        self._base = A()

    def bar(self):
        self.foo()  # self._base.foo() will work

def main():
    B().foo()  #1. works
    B().bar()  #2. attribute not found error

Why does the call to self.foo() inside bar() doesn't go throw delegate while #1 works? To solve it with decorator without hardcode self._base.foo(), must I write method decorator for each method need _base inside B? I try to cut down the boilerplate code.


Solution

  • This is happening because the behavior of decorators is (at least to me) somewhat counterintuitive.

    Here's what happens when you check the types of all the objects involved:

    >>> type(b)
    <class 'Wrapper'>
    
    >>> type(b.wrapped)
    <class 'B'>
    
    >>> type(b.wrapped._base)
    <class 'A'>
    

     

    Because b is really an instance of Wrapper and not B, when you call b.foo(), what's happening is (psuedocode):

    b.foo() --> Wrapper.__getattr__(foo) --> wrapped._base.foo
    

    So foo is coming from the Wrapper object's __getattr__ -- not the Bar object's.

     

    The problem is that in defining bar this way:

    def bar(self):
        self.foo()
    

    self does not refer to the Wrapper object's foo attribute, but the Bar object's foo attribute, which doesn't exist.

    So, when you call b.bar() the method chain is:

    b.bar() --> Wrapper.__getattr__(bar) --> wrapped.bar() --> wrapped.foo()
    

     

    You can fix this by having Bar invoke self._base.foo().