2

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!!!! =)
lgvaz
  • 452
  • 6
  • 17
  • This sounds like a better fit for subclassing. (And note that decorator in Python means something different; better to refer to this as a wrapper.) – Daniel Roseman Jun 19 '18 at 14:48
  • @DanielRoseman, I want to add functionalities at run-time, so no subclassing =/ – lgvaz Jun 19 '18 at 14:51
  • 1
    Is this the same situation? [reclassing an instance](https://stackoverflow.com/questions/990758/reclassing-an-instance-in-python) – Zev Jun 19 '18 at 14:51
  • 1
    @Quazar this is python, everything happens at runtime, even class definitions. What exactly do you mean? You only have access to instances? – FHTMitchell Jun 19 '18 at 14:52
  • @FHTMitchell, I mean that I want to user to be able to add new functionalities as they wish, for example, they first build a dog, then make it big, then make it brown, etc... Again, I'm open to other design patterns that achieve this functionality. – lgvaz Jun 19 '18 at 14:55

1 Answers1

1

Simple way

Using different design pattern -- use dynamic attributes:

class Animal:

    sound: str = None
    big: bool = False
    color: str = None    

    def speak(self) -> str:
        if self.sound is None:
             raise NotImplemnetedError()
        return self.sound.upper() if self.big else self.sound

Then you can have a dog (which all woof)

class Dog(Animal):    
     sound = 'woof!'

and an instance can be big

mydog = Dog()
mydog.big = True
mydog.speak()
# WOOF!

and be brown

mydog.color = 'brown'
mydog.color  # 'brown'

Complex way

If you do want to edit instance methods, you can do it like so (using your pattern from the OP)

import types
import functools

def bigify(inst: Animal) -> None:
    '''Note this modifies the object in place. Implement a `__copy__()` 
    method and call `copy.copy` on inst and return new inst if you 
    don't want this behaviour.
    '''

    old_method = inst.sound  # need to store the old one somewhere

    @functools.wraps(inst.sound)  # preserve __doc__
    def inner(self):
        return old_method().upper()

    inst.sound = types.MethodType(inner, inst)

bigify(dog)

dog.speak()  # WOOF!
FHTMitchell
  • 11,793
  • 2
  • 35
  • 47