Overview of Iterators vs Generators in Python
In the last guide, we walked through how we could build our own custom iterator. And we saw how we could leverage the tools such as Dunder iter and Dunder next in order to build out an infinite lineup and in many cases that will be the approach that you want to take whenever you need to build your own custom behavior into the iteration process.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

But another option that you will have access to is to build what is called a generator. And that's what we're going to walk through in this guide. So we're actually going to replicate the exact same behavior but we're going to use a different tool. And then you're going to be able to see side by side what those implementations look like so you can see which option you prefer.

So I'm going to quit out of this. And I created a generator file that right now simply has the exact same list that we were working with before.

large

So now I'm going to create a class and I'm going to call this my class infiniteLineup just so we have two different class names when you want to see them side by side. And in our infinite lineup, I'm going to start off with an init method so I'm gonna give a def double underscore init followed as usual by self and then players. So so far everything is pretty much the same.

def __init__(self, players):

I'm gonna say self.players is equal to players so we can bring in that players list and now I'm going to create a lineup method so def lineup(self) so I'll have access to players and inside of here I'm going to declare my max lineup so I'm just going to create a variable here called lineup_max and set this equal to the length of self.players. It's going to bring in the total list of players or the count of how many players are there and then I'm going to set the index equal to 0.

def lineup(self):
    lineup_max = len(self.players)
    idx= 0

Now part of the reason why I like this approach if you remember back to the last one where we had to implement quite a bit of custom behavior with all of those negative indexes and had to play around with the way that we constructed the loop. What I'm going to do here is going to be a little bit more straightforward or at least I think it's going to be easier to follow along with.

And that is because I'm going to create on purpose an infinite loop so I'm just going to say while True so this means that this is going to continue until we tell it to stop. I'm going to have a conditional inside of here. So I'm going to say if the index is less than lineup_max then I want to and, now do not let this part scare you if you've never seen this keyword.

This is what generator's use it's called the yield keyword. Now, this is a special keyword in python and essentially what it's doing is exactly what it says, it's yielding to the generator so I'm going to say yield to self.player's index. Else I want to set the index equal to 0 so we're resetting the index and then I still want to yield to self.players and then index. Then from there what I want to do while still inside the loop so still within this while loop I want to increment the index so I'm gonna say increment that by 1 and then that is all that we need to do.

 while True:
            if idx < lineup_max:
                yield self.players[idx]
            else:
                idx = 0
                yield self.players[idx]

            idx += 1

Now, I also want to add a few best-practice items so let's add a Dunder repr and Dunder string function here. So I'll add a repr function which will take in self and then this is going to return that entire object so I'll say return a formatted string and we'll just wrap this so it looks like a full object. So I'll say infiniteLineup and then pass in the values so will use our curly braces and say self.players and then close it off and then we'll create something pretty similar for the Dunder string method. And once again this is just so we can get some practice on seeing what this like.

So here you can say infiniteLineup with the players and then just pass in that list of players in something that looks a little bit prettier than what we had with repr.

  def __repr__(self):
        return f'InfiniteLineup({self.players})'

    def __str__(self):
        return f"InfiniteLineup with the players: {', '.join(self.players)}"

And so now this is the full implementation so let's verify that it works and then we'll walk through exactly what's going on here.

So I'm going to give us some room. And one thing you might find interesting is even though we didn't create a Dunder iter or Dunder next method the way that we did in the last guide we're actually going to have access to a pretty similar API when it comes to calling them.

So let's start off by creating a full_lineup variable and this is going to instantiate our InfiniteLineup which will take our list of players. And now I am going to create an instance of lineup so I'll call astros_lineup and we'll set this equal to the full_lineup and then because if you notice here at the top we created a lineup method. So we simply need to call that right here so I'm gonna say full_lineup.lineup and then because it's a function we need to call it just like this.

And now let's test this out first with our repr and string functions. So I'll say print repr and then pass in the astros_lineup and do the same thing with string just so we see that everything's working. We don't have any typos or anything like that. So I'm going to come here python generators and you can see that so far so good.

large

It's simply printing out the object items and that's because we don't have anything inside of them yet so it doesn't look like we have any typos or anything. Now in between here, this is where we're going to actually call this so as you notice it didn't show us any players or anything like that and it's because we haven't started the process of generating them yet. So now I'm gonna say print and then say next just like we did when we build out our own iterators.

I mean it's a print next and then just call astros_lineup and then that is all that we have to do. So let's just create three of these so this should print out Springer, Bregman, and Altuve because it's going to start off with one then we call next again it'll move down the chain and then call Altuve for the third one.

So let's come over here, run it again. And as you can see that is working properly.

large

So right now you can see that we have each one of those three items and that is exactly what we're looking for.

Now let me come over here. Let's create a few more of these so we'll just go through the lineup because this is an infinite lineup so we want the ability to keep on moving through it and so I run this again you can see it is working very nicely.

large

You notice that and let's just double-check it to make sure we don't have a bug. We have Springer all the way down Tucker's the last one and then it starts over at Springer once again so this is working beautifully. And then we can also see down here that you notice when we printed out string here and repr notice here where it is not printing out the actual data and this is a very important thing to keep in mind.

Whenever we created just our own plain types of string methods and are under string methods and Dunder repr ones on plain classes remember there how it was returning the string or whatever hardcoded item that we sent along. Well here, notice this is overwriting that behavior and without us actually saying that we are creating a generator.

The fact that we and this is where all the magic happens for generators. The fact that we have yield right here and yield here and then we're calling next automatically tells python that we are creating a generator object. Notice that we didn't say generator anywhere when we created the class or the object we simply created a standard class. But whenever you put yield inside that is telling Python hey watch out we have a generator coming you need to be able to work with next and you do not have to define it.

That's a reason why when we print this out the actual Python language is defining our repr and string so it still gives us data. It still tells us that we're calling the infinite lineup and then we're calling the lineup method and it shows where in memory it's doing that but it's actually doing that automatically for us.

So this is something that is a little bit different than what we used last time and now let's look at this right next to each other so I'm going to open up the last guide that we had and down below you can see this is our current version. So right here we used our iter and our next methods and we built our own iteration.

large

and notice here that we had to do quite a bit of work to get this working properly. We had to declare our counter, we had to work with our negative index values right here on length, and there were just a number of things that we had to do to get this working properly we had to create our own next function and declare an iter function and this worked perfectly, but it is not quite as clear in my personal opinion as what we have here in our current example.

Typically whenever I am creating my own custom behaviors so if I were asked to build out a feature like this usually I would look to a generator and that's mainly just because I think it's a little bit easier to implement. And I think it's also pretty easy to read too.

So right here we don't have to have all of that extra boilerplate code we don't have to declare our own Dunder next method or Dunder iter but instead, we've simply taken our values, we create our own custom function here and then inside of it we can simply call the yield and then that is going to provide all of that next type of behavior. And so I think this is a really nice way of being able to implement a custom iteration process.

Code

class InfiniteLineup:
    def __init__(self, players):
        self.players = players

    def lineup(self):
        lineup_max = len(self.players)
        idx = 0

        while True:
            if idx < lineup_max:
                yield self.players[idx]
            else:
                idx = 0
                yield self.players[idx]

            idx += 1

    def __repr__(self):
        return f'InfiniteLineup({self.players})'

    def __str__(self):
        return f"InfiniteLineup with the players: {', '.join(self.players)}"


astros = [
  'Springer',
  'Bregman',
  'Altuve',
  'Correa',
  'Reddick',
  'Gonzalez',
  'McCann',
  'Davis',
  'Tucker'
]

full_lineup = InfiniteLineup(astros)
astros_lineup = full_lineup.lineup()

print(next(astros_lineup))
print(next(astros_lineup))
print(next(astros_lineup))
print(next(astros_lineup))
print(next(astros_lineup))
print(next(astros_lineup))
print(next(astros_lineup))
print(next(astros_lineup))
print(next(astros_lineup))
print(next(astros_lineup))
print(next(astros_lineup))
print(next(astros_lineup))

print(repr(full_lineup))

print(str(full_lineup))