Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pythonic way to apply multiple class methods to list of objects

Tags:

python

class

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?

like image 364
covalent47 Avatar asked May 12 '21 14:05

covalent47


2 Answers

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'
like image 142
Mustafa Aydın Avatar answered Oct 08 '22 20:10

Mustafa Aydın


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
like image 1
Pranav Hosangadi Avatar answered Oct 08 '22 21:10

Pranav Hosangadi