0

I am trying to create a wrap for a class _class1_ (probably, another class) that will have inside a list of several instances of _class1_, some additional variables, some functions that will operate with this list and also wraps for (nearly) all the functions of _class1_, that should actually just call the corresponding functions for all the _class1_ objects stored in the list with the same set of parameters.

Could anyone suggest a better way to do that then just to implement all this functions (that will iterate through a list and call the corresponding functions from List_class1) by hand?

Example

class class2:
    def __init__(self, N):
         self._N = N
         self.List_class1 = []
         for i in range(0, self._N): 
             self.List_class1.append(class1())

    def function1(self, params)
        for i in range(0, self._N):
               List_class1[i].function1(params)

    def function2(self, params)
        ret = []
        for i in range(0, self._N):
               ret.append(List_class1[i].function2(params))
        return ret

So if a have a number of functions in class1 that I wand to adapt like that, maybe someone can suggest a way to do that wo just rewriting?

Basil
  • 3
  • 2

2 Answers2

0

One way that could work for you is to override the __getattr__ method for your wrapper class. This gets called when an instance attribute can't be found (ie. any functions not implemented in your wrapper that are in your main class).

See this question for something similar, but only with a single class rather than a list of them.

class Thing:
    def __init__(self, name):
        self.name = name

    def func_1(self):
        print(f"func_1: {self.name}")

    def func_2(self):
        print(f"func_2: {self.name}")


class AttributeGroup:
    def __init__(self):
        self.attributes = []

    def add_attribute(self, function):
        self.attributes.append(function)

    def __call__(self, *args, **kwargs):
        for attr in self.attributes:
            attr(*args, **kwargs)


class AllThings:
    things = []

    def __getattr__(self, key):
        attrs = AttributeGroup()
        for thing in self.things:
            attrs.add_attribute(thing.__getattribute__(key))
        return attrs


group = AllThings()
group.things.append(Thing('a'))
group.things.append(Thing('b'))

group.func_1()
group.func_2()

The __getattr__ method in the AllThings class should return a single attribute, hence the FunctionGroup class that just passes the call to all of the functions that it has collected.

The above code should print:

func_1: a
func_1: b
func_2: a
func_2: b

You could also then make the attribute group class iterable in some way so that you could get all attributes in a list, rather than just being able to call all the functions

# AttributeGroup class
def __getitem__(self, item):
    return self.attributes[item]

# main
for each in group.name:
    print(each)

Prints:

a
b
inkychris
  • 1,179
  • 2
  • 12
  • 18
0

First, there are a lot of things about this code that are incorrect, or at least weird:

  • You have a shared class attribute __N holding an empty list—but in __init__, you immediately hide that with an instance variable of the same name, presumably holding a number. So, that class attribute is never going to do anything.
  • There's also no reason for it to be called __N. The only reason for double underscores is when you need to protect against superclasses or subclasses accidentally using the same name for something incompatible.
  • You're using another shared class attribute List_class1, but you're initializing it in your __init__. This makes sense for cases like, e.g., registering all instances of a class with the class, but it doesn't make sense for cases where you're trying to create a list per instance. When you construct a dozen class2(10)s, you want each one to have 10 class1 instances, not all of them to share the same 120, right?
  • Your function1 and function2 aren't methods, and can't be called, because they take no self. Or, rather, they do take a self, but under the unusual name params. Which means they end up passing self as the params to every function in class1.

Anyway, it looks like what you're trying to do is create a sort of generic proxy, but one that does a "fan-out" proxy to a list of objects, instead of just proxying to a single object.

So, the same basic techniques for generic proxies will work.


For example, you can do it dynamically with __getattr__:

class class2:
    def __init__(self, N):
        self.c1s = [class1() for _ in range(N)]

    def __getattr__(self, name):
        if name.startswith('_'):
            raise AttributeError(name)
        methods = []
        for c1 in self.c1s:
            attr = getattr(c1, name)
            if callable(attr):
                methods.append(attr)
        if methods:
            def _map(*args, **kw):
                return [method(*args, **kw) for method in methods]
            return _map
        raise AttributeError(name)

class class1:
    def function1(self, x, y):
        return x+y
    def function2(self):
        return 'function 2!'

c2 = class2(3)
print(c2.function1(2, 3))
print(c2.function2())

This will print [5, 5, 5] and then ["function 2!", "function 2!", "function 2!"].


Or, since you only want to hold class1 instances, you can do it statically. This has the advantage that class2.function2 will be a real method that you can see in the __dict__, so your interactive interpreter can autocomplete it, etc.

class class2:
    def __init__(self, N):
        self.c1s = [class1() for _ in range(N)]

for name, value in inspect.getmembers(class1, callable):
    if name.startswith('_'):
        continue
    setattr(class2, name, lambda self, *args, _v=value, **kw: [_v(c1, *args, **kw) for c1 in self.c1s])

OK, that last one could probably be a bit prettier instead of a horrible one-liner, but hopefully you get the idea.

abarnert
  • 354,177
  • 51
  • 601
  • 671