NOTE: I'm not asking about common python decorators, but about the decorator design pattern.
I want to write a decorator that is able to modify a function called by the concrete component, the following code sample illustrates my problem:
In [2]: class Animal:
...: def sound(self):
...: raise NotImplementedError
...:
...: def speak(self):
...: print(self.sound())
...:
In [3]: class Dog(Animal):
...: def sound(self):
...: return 'woof!'
...:
In [4]: class Bigfy:
...: def __init__(self, animal):
...: self.animal = animal
...:
...: def sound(self):
...: return self.animal.sound().upper()
...:
...: def speak(self):
...: return self.animal.speak()
...:
In [5]: dog = Dog()
...: dog.speak()
...:
woof!
In [6]: big_dog = Bigfy(Dog())
...: big_dog.sound()
...:
Out[6]: 'WOOF!'
In [7]: big_dog.speak()
woof! # I want 'WOOF!' here
The method that I want to enhance functionality is sound
, but this method is not directly called by the client and is instead internally called by speak
so all the wraps on sound
take no effect.
Is it possibly to achieve what I want using the decorator design pattern? If not what design pattern I should take a look?
Edit: Thanks everyone for your quick answers, following the pattern from @FHTMitchell I reached the following solution:
In [1]: import inspect
In [2]: class Animal:
...: def sound(self):
...: raise NotImplementedError
...:
...: def speak(self):
...: print(self.sound())
...:
...: # Key change
...: @property
...: def unwrapped(self):
...: return self
...:
In [3]: class Dog(Animal):
...: def sound(self):
...: return 'woof!'
...:
In [4]: class BaseWrapper:
...: def __new__(cls, animal, **kwargs):
...: self = super().__new__(cls)
...: self.__init__(animal, **kwargs)
...:
...: # Automatically points unwrapped methods to last wrapper
...: for attr in dir(animal):
...: # Don't get magic methods
...: if attr.startswith('__') or attr.startswith('old'):
...: continue
...:
...: value = getattr(animal, attr)
...: if inspect.ismethod(value):
...: # Store old method
...: setattr(self, 'old_' + attr, value)
...: # Points to new method
...: setattr(animal.unwrapped, attr, getattr(self, attr))
...:
...: return self
...:
...: def __init__(self, animal):
...: self.animal = animal
...:
...: # Delegate all non-implemented attrs calls to wrapped class
...: def __getattr__(self, name):
...: return getattr(self.animal, name)
...:
...: # Helps with editor auto-completion
...: def __dir__(self):
...: dir_list = super().__dir__()
...: dir_list.extend(self.animal.__dir__())
...:
...: return dir_list
...:
In [5]: class Bigify(BaseWrapper):
...: def sound(self):
...: return self.old_sound().upper()
...:
In [6]: class Angrify(BaseWrapper):
...: def sound(self):
...: return self.old_sound() + '!!!'
...:
In [7]: class Happify(BaseWrapper):
...: def sound(self):
...: return self.old_sound() + ' =)'
...:
In [8]: big_angry_dog = Happify(Angrify(Bigify(Dog())))
...: big_angry_dog.speak()
...:
WOOF!!!! =)