简体   繁体   中英

How to wrap all python superclass methods with subclass internal state?

I am aware of this question but I don't think it covers my case.

I have an external Dummy class with many methods, all using an instance attribute. Instead of using this instance attribute, I want to be able to pass it as an argument. My solution is to keep a collection of dummies, and use the one with the appropriate attribute when necessary.

class Dummy:

    def __init__(self, prefix="dum"):
        self.prefix = prefix

    def toto(self):
        return f"{self.prefix}_toto"

    def titi(self):
        return f"{self.prefix}_titi"

    def tata(self):
        return f"{self.prefix}_tata"


class DynamicDummy:

    def __init__(self):
        self.dummies = {}

    def _get_dummy(self, prefix):
        dummy = self.dummies.get(prefix)
        if dummy is None:
            dummy = Dummy(prefix)
            self.dummies[prefix] = dummy
        return dummy

    def toto(self, prefix):
        dummy = self._get_dummy(prefix)
        return dummy.toto()

    def titi(self, prefix):
        dummy = self._get_dummy(prefix)
        return dummy.titi()

    def tata(self, prefix):
        dummy = self._get_dummy(prefix)
        return dummy.tata()

The thing is, there are way more than 3 methods, and I want it to be automatic, such that I don't have to change my DynamicDummy everytime there is a change in Dummy . I have never used metaclass before, so maybe they are the solution, but I can't make them work with the dummies dictionnary, which is an instance attribute.

I am willing to go for a solution that makes it automatic, but also with an other solution altogether for the "dynamicity" problem.

following @buran advice, I modified the __getattribute__ method.

class SmartDynamicDummy(Dummy):

    def __init__(self):
        self.dummies = {}

    def _get_dummy(self, prefix):
        dummy = self.dummies.get(prefix)
        if dummy is None:
            dummy = Dummy(prefix)
            self.dummies[prefix] = dummy
        return dummy

    def _wrapper(self, func, func_name):
        @wraps(func)
        def wrapped(*args, **kwargs):
            args = list(args)
            prefix = args.pop(0)
            args = tuple(args)
            dummy = self._get_dummy(prefix)
            dummy_func = getattr(dummy, func_name)
            return dummy_func(*args, **kwargs)

        return wrapped


    def __getattribute__(self, name):
        attr = super(SmartDynamicDummy, self).__getattribute__(name)
        if isinstance(attr, types.MethodType) and not name.startswith('_'):
            # attr.__name__ and name can be different if attr is a decorated function
            attr = self._wrapper(attr, name)
        return attr

If you change your prefix attribute on the target instance before you make your call, it will stay changed and just work- no need for a collection of instances with different values for prefix . The one case were the value could change midway is if your application is paralellized using threads - in that case, one of the other methods could be called on DynamicDummy requiring a different "prefix" before another call is over. This is the kind of problems that is solvable with Locks .

Metaclasses don't really have a role here. Sure, one could devise a complicated thing involving a metaclass, but we are just dealing iwth ordinary attribute setting;

So, in other words: if your program does not run in parallel, once you enter SmartDummy.toto , and this calls a Dummy instance .toto() , there is no way another method of the same Dummy instance will be called until both calls are resolved - so you can just set the desired value in SmartDummy.toto , before calling the method in the associated dummy instance.

If your code does run in parallel, but uses the multiprocessing model: the samething applies: in each process, instances of SmartDummy are run as if in a serial program, and no external thing can change the prefix attribute before SmartDummy.toto resolves.

If your code is parallel, but using asyncio , midway changes can just take place if the methods toto , tata , etc... are themselves async methods and yield the control to the async loop. If they are not declared as "async" it is guaranteed no code will run in parallel that could modify the attribute value (not even by some other function or method Dummy.toto calls: if it is not "async", it can't yield execution to the loop. It can even schedule new tasks to be run, but those will only be touched when you return the execution to the mainloop, and from a non-async function that happens the end of the function itself.

So, we are back to: just save the attribute value, make your call, and restore it. Add a Lock provision for the multi-thread case and you are fine. Assuming you don't have access to the Dummy code, and that each instance of DynamicDummy fas one instance Dummy associated, we can create a lock at the DynamicDummy instance. If all DynamicDummy s would hare a single instance of Dummy , the lock would have to be a class attribute instead.

To call the wrapped methods in a transparent way, the design you got too is good: I am just changing it to use __getattr__ as it is simpler:

import threading

class DymamicDummy:
    def __init__(self, ...):
        ...
        self.dummy = Dummy()
        self.lock = threading.Lock()
        
    def _dispatcher(self, method):
        # the same thign you found your design: 
        # 'prefix' is always the first positional argument. Change it to a named parameter if desired.
        def wrapper(prefix, *args, **kwargs):
            with self.lock:
                original_prefix = self.dummy.prefix
                self.dummy.prefix = prefix
                try:
                    result = method(*args, **kwargs)
                finally:
                    self.dummy.prefix =self.dummy.prefix
            return result
            
        return wrapper
        
    def __getattr__(self, method_name):
        # using __getattr__ instead of __getattribute__:
        # this only gets called for methods or attributes that do not exist
        # on the current instance anyway: so no need to further testing,
        # just extract the value from Dummy.
        method = getattr(self.dummy, method_name)
        if not callable(method):
            return method
        return _dispatcher(method)
    

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM