2

I have the following yield function:

def yield_numbers(start=1, end=1e6):
    num = start
    while num <= end:
        yield num
        num += 1

I would like to build a class to replicate the most basic functionality of the yield expression to understand how it works a bit more. Here is what I have so far:

class YieldNumbers:
    def __init__(self, start=1, end=1e6):
        self.start = int(start)
        self.end = int(end)
        self._current_val = None
        self._closed = False
    def __iter__(self):
        if self._closed: raise StopIteration
        return self
    def close(self):
        self._closed = True
    def send(self, value):
        return self.__next__(value)
    def throw(self, exc_type):
        assert isinstance(exc_type, Exception)
        return self.__next__(exc_type=exc_type)
    def __next__(self, value=None, exc_type=None):
        if self._closed: raise StopIteration
        if exc_type: raise exc_type
        self._current_val = value if value else self.start if not self._current_val else self._current_val + 1
        if self._current_val > self.end: self._closed=True; raise StopIteration
        return self._current_val

And using it:

for i in YieldNumbers(start=1,end=3):
    print (i)
1
2 
3
>>> y=YieldNumbers()
>>> next(y)
1
>>> y.close()
>>> next(y)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in __next__
StopIteration

Is this similar to how yield works (at its most basic level). If not, what am I missing here? Again, this isn't to re-invent the wheel or to make something robust, it's more to understand how yield works at a conceptual level.

David542
  • 104,438
  • 178
  • 489
  • 842
  • This question might be useful to look at: https://stackoverflow.com/questions/14183803/what-is-the-difference-between-raise-stopiteration-and-a-return-statement-in-gen – kimbo Feb 27 '20 at 23:16
  • It honestly looks like an extremely good implementation of the iterator protocol. Maybe you could slightly improve it by making it more general by, for example, coding having a counter 'n' which increases every time __next __ is called and having __next __ so that, if 'n' is greater than the ending value, you raise the StopIteration, otherwise you use a function to generate the associated value. For this specific implementation though it's practically perfect – PMM Feb 27 '20 at 23:22

1 Answers1

3

Although the behaviour is the same in your example, this is not similar to how yield works. Fundamentally, yield pauses the execution of a generator function, and it resumes from the same place when next is called; and the generator function begins paused, so the initial code is not executed until the first call to next.

It is possible, but non-trivial to transform code using yield into code which doesn't use yield. To simulate the way that the generator works, we need the instance to know where it should pretend to "resume" from, so let's call that resume_point. I didn't bother with some details like sending data back to the generator, exception handling, or closing; but you get the idea.

class Example:
    def __init__(self, start, end):
        self.start = start
        self.end = end
        self.resume_point = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.resume_point == 0:
            # simulates the beginning of your generator function
            self.num = self.start
            # simulates checking the `while` loop condition
            if self.num <= self.end:
                # next time we should continue from the `yield`
                self.resume_point = 1
                # `return` simulates pausing the generator function
                return self.num
            # simulates the end of your generator function
            self.resume_point = 2
            raise StopIteration
        elif self.resume_point == 1:
            # simulates continuing from the `yield` in your generator function
            self.num += 1
            # simulates checking the `while` loop condition
            if self.num <= self.end:
                # redundant, but it shows what happens
                self.resume_point = 1
                # `return` simulates pausing the generator function
                return self.num
            # simulates the end of your generator function
            self.resume_point = 2
            raise StopIteration
        elif self.resume_point == 2:
            # simulates resuming from after your generator function returned
            self.resume_point = 2
            raise StopIteration
kaya3
  • 47,440
  • 4
  • 68
  • 97
  • just to clarify -- "0" would mean the start of the generator, "1" would mean during the active execution, and "2" would mean when it exits, correct? – David542 Feb 27 '20 at 23:47
  • In this example, yes, 0 is the start of the function, 1 is the `yield`, and 2 is the end. There could be more resumption points for different examples, generally one per `yield`. – kaya3 Feb 27 '20 at 23:52