I have a class with some built-in methods. This is a abstracted example of what the class might look like:
class Foo:
def __init__(self):
self.a = 0
self.b = 0
def addOneToA(self):
self.a += 1
def addOneToB(self):
self.b += 1
For the sake of simplicity, I've reduced the built-in methods to 2 total, but in actuality my class has closer to 20.
Next I have another class that is designed to work on a list of Foo
instances.
class Bar:
def __init__(self, fooInstances):
self.fooInstances = fooInstances
# Bar([Foo(), Foo(), Foo()])
What if I wanted to apply one of the Foo
methods to the Foo
instances in Bar
?
class Bar:
# ...
def addOneToA(self):
for fooInstance in self.fooInstances:
fooInstance.addOneToA()
def addOneToB(self):
for fooInstance in self.fooInstances:
fooInstance.addOneToB()
The example above is one way of doing what I described, but it seems like a great deal of repetitive code to do this if there were 20 class methods of Foo
. Alternatively, I could do something like this:
class Bar:
# ...
def applyFooMethod(self, func, *args):
for fooInstance in self.fooInstances:
fooInstance.func(args)
But I would prefer to have something that would allow me to call .addOneToA()
on Bar
and have it be applied to all Foo
instances in Bar
. Is there a clean way to do this without defining all methods of Foo
inside Bar
?
One way is to override __getattr__
of Bar
:
class Bar:
def __init__(self, fooInstances):
self.fooInstances = fooInstances
def __getattr__(self, attr):
try:
getattr(self.fooInstances[0], attr)
except AttributeError:
raise AttributeError(f"'Bar' object has no attribute '{attr}'")
else:
def foo_wrapper(*args, **kwargs):
for foo_inst in self.fooInstances:
getattr(foo_inst, attr)(*args, **kwargs)
return foo_wrapper
__getattr__
on Bar
is called if the attribute lookup on Bar
object fails. Then we try and see if a Foo
instance has that attribute; if not, then raise an AttributeError
because neither Bar
nor Foo
accepts that attribute. But if Foo
does have it, we return a function that, when called, invokes the method (attr
) on each instant of Foo
residing in Bar
object.
Usage:
...
# changed this method in Foo to see the passing-an-argument case
def addOneToA(self, val):
self.a += 1
print(f"val = {val}")
...
>>> bar = Bar([Foo(), Foo(), Foo()])
>>> bar.addOneToB()
>>> [foo.b for foo in bar.fooInstances]
[1, 1, 1]
>>> bar.addOneToA(val=87) # could also pass this positionally
val = 87
val = 87
val = 87
>>> bar.this_and_that
AttributeError: 'Bar' object has no attribute 'this_and_that'
Another way is to use setattr()
to create a function which calls applyFooMethod()
when you construct a bar
object. This way, dir(bar)
will show the methods of Foo
.
class Bar:
def __init__(self, fooInstances):
self.fooInstances = fooInstances
foo0 = fooInstances[0]
for method_name in dir(foo0):
method = getattr(foo0, method_name)
# Make sure it's callable, but not a dunder method
if callable(method) and not method_name.startswith("__"):
# Make a lambda function with a bound argument for method_name
# We simply need to call applyFooMethod with the correct name
mfunc = lambda m=method_name, *args: self.applyFooMethod(m, *args)
# Set the attribute of the `bar` object
setattr(self, method_name, mfunc)
def applyFooMethod(self, func_name, *args):
for fooInstance in self.fooInstances:
func = getattr(fooInstance, func_name)
func(*args)
Then, you can run it like so:
foos = [Foo(), Foo(), Foo(), Foo()]
bar = Bar(foos)
dir(bar)
# Output:
# [...the usual dunder methods...,
# 'addOneToA',
# 'addOneToB',
# 'applyFooMethod',
# 'fooInstances']
Now, we can call bar.addOneToA()
:
bar.addOneToA()
for f in foos:
print(f.a, f.b)
bar.addOneToB()
for f in foos:
print(f.a, f.b)
Which first increments all a
values, and then all b
values.
1 0
1 0
1 0
1 0
1 1
1 1
1 1
1 1
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With