5

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?

4
  • It might be worth sharing some more context for this, it's possible that improvements could be made to the overall design. Commented May 12, 2021 at 15:05
  • @AMC absolutely, I'll update the question with some context in a bit. Broadly, my implementation of the Bar class is a filter that works on the list of Foo objects and basically narrows down the list based on parameters. For example, I could create a Bar class like bar = Bar([Foo(), Foo(), Foo()]) then run something like bar.filterElementsWith("a", "<=", 5) and this would eliminate all Foo instances in the fooInstances list where a is less than or equal to 5. The actual code is a bit more involved but I'll update the question with this information when I get a chance. Commented May 12, 2021 at 15:16
  • @covalent47 - this question looks similar to this post: stackoverflow.com/questions/37075680/run-all-functions-in-class Commented May 12, 2021 at 15:40
  • Do you really need a separate class to handle mapping a function over a list? Commented May 12, 2021 at 15:55

2 Answers 2

6

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'
Sign up to request clarification or add additional context in comments.

6 Comments

wouldn't you also have to check for callable on each attribute? (like callable(getattr(fooInstance, attr))
@bogdan_ariton right, but it will raise an error anyway and I believe it is okay to leave the error raising part to Python itself for noncallables of Foo..
from what I can tell you can end up calling an uncallable attribute in the else clause, this can fail.
@bogdan_ariton In that case, a descriptive TypeError will be automatically raised. How should the program behave in your opinion? Manually handling the "attribute but not callable" case?
A great solution and exactly on the lines of what I was looking for. Thanks for the help.
|
1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.