Reputation: 627
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.
Upvotes: 1
Views: 728
Reputation: 627
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
Upvotes: 2
Reputation: 110801
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)
Upvotes: 0