内部调用了Decorator/Wrapper增强方法

2024-04-25 08:09:14 发布

您现在位置:Python中文网/ 问答频道 /正文

注意:我不是问常见的python decorator,而是问decorator设计模式。你知道吗

我想编写一个能够修改由具体组件调用的函数的装饰器,下面的代码示例说明了我的问题:

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

我想要增强功能的方法是sound,但是这个方法不是由客户机直接调用的,而是由speak在内部调用的,因此sound上的所有包装都没有效果。你知道吗

使用decorator设计模式是否可能达到我想要的效果?如果不是什么设计模式,我应该看看?你知道吗


编辑:感谢大家的快速回答,按照@FHTMitchell的模式,我得出了以下解决方案:

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!!!! =)

Tags: inselfreturndefdiroldclassattr
1条回答
网友
1楼 · 发布于 2024-04-25 08:09:14

简单方法

使用不同的设计模式使用动态属性:

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

那你就可以养条狗了

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

一个实例可能很大

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

而且是棕色的

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

复杂方式

如果确实要编辑实例方法,可以这样做(使用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!

相关问题 更多 >