How to overload __next__ inside __init__?

65 Views Asked by At

I am writing range generator. Depending on whether the step is positive or negative, __next__ should compare start and end with <= or >=. I would like to check if step is positive once, and define __next__ depending on that comparison. This is what I did (to simplify, allow only exactly three inputs):

class MyRange:
    def __init__(self, *args):
        self.min, self.max, self.step = args[0] - args[2], args[1], args[2]

        if self.step > 0:
            self.__next__ = self.next_pos
        else:
            self.__next__ = self.next_neg

    def __iter__(self):
        return self

    def __next__(self):
        pass
    
    def next_pos(self):
        self.min += self.step
        if self.min >= self.max:
            raise StopIteration
        return self.min

    def next_neg(self):
        self.min += self.step
        if self.min <= self.max:
            raise StopIteration
        return self.min

If __next__ is not defined outside of __init__, then I get an error: TypeError: iter() returned non-iterator of type 'MyRange'.

I assumed that defining it as above would first initialise it according to the def __next__(self), and then overwrite in __init__, but the order is the opposite. Why is it not being overloaded? How can I overwrite __next__ depending on the step when creating an instance?


I know that I could store next_pos or next_neg inside self.my_comparison, and define

    def __next__(self):
        return self.my_comparison()

but I am wondering how I can overload __next__ inside __init__ and why it doesn't work in my case.

1

There are 1 best solutions below

0
jsbueno On

Python dunder (or magic) methods are always looked on the class of an object, and never picked from the instance. Assigning an ordinary function to an instance that will work as a method will work, although it won't get the self argument injected by the Python runtime - but dunder methods are called by the runtime itself, and they are picked from the appropriate class slot for each functionality.

That is easy to work around: you can use your class defined __next__ method to call another, instance bound callable, as an ordinary, "user side" call. You can then make this binding in __init__ as you had devised:

class MyRange:
    def __init__(self, *args):
        self.min, self.max, self.step = args[0] - args[2], args[1], args[2]

        if self.step > 0:
            self._user_next = self.next_pos
        else:
            self._user_next = self.next_neg

    def __iter__(self):
        return self

    def __next__(self):
        return self._user_next()
    
    def next_pos(self):
        ...

    def next_neg(self):
        ...

Note that I wrote above that callables bound to the instance in this way won't get "self" injected by Python - however, this code is not assigining an ordinary function there: when we write self.user_method = self.method - the self.method part creates a "bound method" - a callable that will already insert the self to the underlying function (unbound method) when called. And that bound method is then assigned as a common instance attribute. When we execute self.usermethod(), first usermethod is retrieved from the instance, with no transformation mechanism (such as self arg injection) and then called.