Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does a yield from inside __next__() return generator object?

Tags:

I am using yield to return the next value in the __next__() function in my class. However it does not return the next value, it returns the generator object.

I am trying to better understand iterators and yield. I might be doing it in the wrong way.

Have a look.

class MyString:
    def __init__(self,s):
        self.s=s

    def __iter__(self):
        return self

    def __next__(self):
        for i in range(len(self.s)):
            yield(self.s[i])

r=MyString("abc")
i=iter(r)
print(next(i))

This returns:

generator object __next__ at 0x032C05A0

like image 987
sachin Avatar asked Jun 20 '16 19:06

sachin


People also ask

Does yield return a generator?

When you use a yield keyword inside a generator function, it returns a generator object instead of values. In fact, it stores all the returned values inside this generator object in a local state. If you have used the return statement, which returned an array of values, this would have consumed a lot of memory.

What does a generator return what does a generator return?

Simply speaking, a generator is a function that returns an object (iterator) which we can iterate over (one value at a time).

What happens when a generator encounters a yield statement?

The yield statement returns a generator object to the one who calls the function which contains yield, instead of simply returning a value.

What is the purpose of the return statement in a generator?

A return statement in a generator, when executed, will make the generator finish (i.e. the done property of the object returned by it will be set to true ). If a value is returned, it will be set as the value property of the object returned by the generator.


2 Answers

next pretty much just calls __next__() in this case. Calling __next__ on your object will start the generator and return it (no magic is done at this point).


In this case, you might be able to get away with not defining __next__ at all:

class MyString:
    def __init__(self,s):
        self.s=s

    def __iter__(self):
        for i in range(len(self.s)):
            yield(self.s[i])
        # Or...
        # for item in self.s:
        #     yield item

If you wanted to use __iter__ and __next__ (to define an iterator rather than simply making an iterable), you'd probably want to do something like this:

class MyString:
    def __init__(self,s):
        self.s = s
        self._ix = None

    def __iter__(self):
        return self

    def __next__(self):
        if self._ix is None:
            self._ix = 0

        try:
            item = self.s[self._ix]
        except IndexError:
            # Possibly reset `self._ix`?
            raise StopIteration
        self._ix += 1
        return item
like image 52
mgilson Avatar answered Jan 26 '23 01:01

mgilson


Let's take a look at the purpose of the __next__ method. From the docs:

iterator.__next__()

Return the next item from the container. If there are no further items, raise the StopIteration exception.

Now let's see what the yield statement does. Another excerpt from the docs:

Using a yield expression in a function’s body causes that function to be a generator

And

When a generator function is called, it returns an iterator known as a generator.

Now compare __next__ and yield: __next__ returns the next item from the container. But a function containing the yield keyword returns an iterator. Consequently, using yield in a __next__ method results in an iterator that yields iterators.


If you want to use yield to make your class iterable, do it in the __iter__ method:

class MyString:
    def __init__(self, s):
        self.s = s

    def __iter__(self):
        for s in self.s:
            yield s

The __iter__ method is supposed to return an iterator - and the yield keyword makes it do exactly that.


For completeness, here is how you would implement an iterator with a __next__ method. You have to keep track of the state of the iteration, and return the corresponding value. The easiest solution is probably to increment an index every time __next__ is called:

class MyString:
    def __init__(self,s):
        self.s = s
        self.index = -1

    def __iter__(self):
        return self

    def __next__(self):
        self.index += 1

        if self.index >= len(self.s):
            raise StopIteration

        return self.s[self.index]
like image 39
Aran-Fey Avatar answered Jan 26 '23 00:01

Aran-Fey