pythonlistoopmetaclassclass-decorator

Index member of class as list in python


Assume I have a simple class like

class Foo:
  def __init__(bar):
    self.x = transform1(bar)
    self.y = transform2(bar)

I'm now interested in generating a class, where I can pass an iterable for bar to the initializer and get back an instance of Foo where i can access the members x and y like iterables of the size of bar, i.e.

x = [1, 2, 3]
foo = Foo(x)
plt.plot(foo.x, foo.y)

I know one could easily do

foo = [Foo(elem) for elem in x]
plt.plot([elem.x for elem in foo], [elem.y for elem in foo])

but this feels verbose and probably is not very efficient. I can roughly imagine a solution with numpy structured arrays, but I'm just curious if there are any standard solutions to this. Perhaps using metaclasses. Googling mostly came up with results for how to get a list of all the members of a class or similar.

If one could even come up with a solution that allows for indexing either the object foo or its members, this would be grand.


Solution

  • If I get this right, you just want to transform all elements in bar at once. just do it, instead of one scalar at a time. Just do it:

    class Foo:
      def __init__(bar):
        self.x = [transform1(el) for el in bar] 
        self.y = [transform2(el) for el in bar]
    

    It is really that simple. There are fancy things if you'd want to run transform1 and transform2 in parallel, using threads or processes, or if you'd like to have all the transforms calculated just as needed, in a lazy way.

    But for plotting your graph, a list will do. And there are even no gains in doing it in a single for loop instead of two list comprehensions - the time taken in the iteration using for itself is negligible.


    If you want to be able to index the instance itself, and get back objects that have the needed attributes, what is necessary is to write a class with the __getitem__ method - and have the objects returned by getitem to have both attributes.

    For that you could use a simpler class, representing your scalar, and depending on what you need, this simpler class can be a namedtuple:

    from collections import namdtuple
    
    ScalarFoo = namedtuple("ScalarFoo", "x y")
    
    class Foo:
      def __init__(bar):
        self.x = [transform1(el) for el in bar] 
        self.y = [transform2(el) for el in bar]
      def __getitem__(self, index):
           return ScalarFoo(self.x[index], self.y[index])
      def __len__(self):
           return len(self.x)
    

    (The __len__ method, combined with the __getitem__ allows Python to use instances of Foo automatically in for-loop iterations)

    Now, if you want to get it really interesting, let's suppose your Foo class, with the scalar application of the transforms exists as in your question - it is possible to "transform" it so that it can operate with sequences.

    Than we get closer to the original metaclass research - and can be achieved by using a class decorator. Class decorators were introduced a long while back to replace some of the uses of metaclasses.

    
    def autosequence(cls):
        """Transforms the received class into a factory,
        so that if a sequence or iterator is passed as the first
        argument to it, a new, sequence class is used. If the
        resulting class is used in an iteration or as a sequence,
        an instance of the original class is returned
        """
        
        class AutoSequence:
            def __init__(self, *args, **kw):
                self.sequence = list(args[0])
                self.other_args = args[1:]
                self.kw = kw
            
            def __getitem__(self, index):
                return cls(self.sequence[index], *self.other_args, **self.kw)
            
            def __len__(self):
                return len(self.sequence)
            
            def __repr__(self):
                return f"Lazy sequence of f{cls.__name__} objects with {len(self)} elements"
            
            
        def factory(*args, **kw):
            if args and hasattr(args[0], "__len__") or hasattr(args[0], "__iter__"):
                return AutoSequence(*args, **kw)
            return cls(*args, **kw)
            
        factory.__name__ = cls.__name__
        return factory
    
    
    
    def transform1(a):
        return a
    
    def transform2(a):
        return a ** 2
    
    
    @autosequence
    class Foo:
        def __init__(self, bar):
            self.x = transform1(bar)
            self.y = transform2(bar)
            
        def __repr__(self):
            return f"{self.__class__.__name__}({self.x}, {self.y})"
        
    

    And here is how this behaves in the interactive interpreter:

    In [24]: a = Foo([1,2,3])                                                                            
    
    In [25]: a[2]                                                                                        
    Out[25]: Foo(3, 9)
    
    In [26]: Foo(4)                                                                                      
    Out[26]: Foo(4, 16)
    
    In [27]: Foo(4).y                                                                                    
    Out[27]: 16
    
    In [28]: a[2].y                                                                                      
    Out[28]: 9
    

    The "factory" function above could be made into a __new__ and inserted into the decorated class, and then the resulting decorated class would behave as the real class - but specially if you have introspecting code and need the Foo class to be real class operating on scalars, you'd better have two separate classes at all - one creating the sequences, the other dealing with scalars.

    In that case you could ust strip the "factory" function, have "autosequence" to return the AutoSequence class itself, and use it like this:

    
    class Foo:
       ...
    
    FooSequence = autosequence(Foo)