Jose: Hi and welcome back.
In this video we're looking at iterables in Python.
In the last video we looked at iterators,
so what in the heck is an iterable?
Well, an iterable is an object
that has an __iter__() method, or an __iter__() method.
So something that looks like this.
Once you define this method in any object,
that is now an iterable.
So let's define something here.
Let's define a FirstHundredIterable.
This FirstHundredIterable just is going
to define this __iter__(), or __iter() method,
and this __iter__() method tells Python that you want
to be able to able to iterate over this iterable,
like in a for loop, or you wanna do some of it,
or you wanna turn it into a list.
What does it have to return?
It has to return an iterator.
So return FirstHundredGenerator().
Now you can do something like
print the sum of FirstHundredIterable,
and it will like it, and it will tell you 4950.
You can also iterate over it
using a for loop and it will be fine.
It will also give you all the numbers there.
So that's what makes a iterable.
Nothing magical, doesn't have to do anything contrived.
For Python, for it to become an iterable,
all you have to do is define this method,
and that method has to return something
that you can call __next__() on.
It has to return an iterator.
All generators are iterators,
so of course this can be a generator.
Now if you want to make this a bit simpler,
which you can do,
Instead of having a separate class returning
the FirstHundredIterator, you can ask yourself ...
There's this __iter__() method here
that's returning an object of this type.
What is FirstHundredGenerator()
in the context of this class?
This is a pretty vague question, but I would say it is self.
Self is always the current object,
and what you're doing here is you're returning an object,
so surely you can do
this.
Now you no longer need this FirstHundredIterable,
and you can just return FirstHundredGenerator() here,
and it will stay like it now.
Now your generator is a bit more confusing
because it's both an iterator and an iterable,
and that can be confusing in some cases,
but it can really be handy in others
because it's much shorter than having a separate class
to do that for you.
Again, the iterable returned an iterator
using this __iter__() method.
So you can do that from the iterator itself as well,
since self is always an iterator, because self is
the object that has this dunder __next__() method.
So I mentioned much earlier in the course something
about for loops and how we needed an object
that had dunder __len__() and dunder __getitem__() defined.
So what's that about?
Let's define another iterable,
and it's going to have a __len__(),
len(self.cars), and I'm going to initialise cars up here.
So let's say we've got self.cars is equal to this
two-element list then we define a dunder __len__() method,
which returns the length of the list, and we also define
a __getitem__() method
which takes in the index that we want to retrieve,
and just returns self.cars[i].
Earlier on the course, we looked at this and I said
that when you have an object that looks like this
that has the __len__() and the __getitem__() method,
you can use for loop on it.
And indeed, if we do for car in AnotherIterable(),
you'll see that this prints out the cars, Fiesta and Focus.
So is this an iterable too?
It doesn't have an __iter__() method.
It doesn't return any generator.
And the answer is yes.
In Python you can have your iterables defined either
with an __iter__() method that returns an iterable,
or you can have an object that has a length and a getitem.
Both of these are iterables and you can use for loops,
you can use __sum__(), you can use __len__() in them ...
Sorry, not __len__(), you can use __list__() in them.
So both of these things are iterables.
Now, okay?
And now we've learned a bit about iterables,
how they're different from iterators,
but they are normally together, hand-in-hand.
An iterator is used to get the next value,
and an iterable is used to go over all the values
of an iterator.
So an iterator sort of lets you go more step-by-step,
in case you want to do that, by calling __next__(),
and the iterable lets you go over all of the elements.
So that's it for this video.
If if doesn't make much sense and you're wondering,
"Well why the hell am I gonna need this?"
Don't worry.
In a few sections' time, you're gonna be like,
"Oh my God, this makes so much sense,"
as we learn about asynchronous Python.
Just giving you a few hints here.
And also, when you want to use or go over a list
that is a bit longer, you can use generators to do that.
Now let me also say, now that we're here,
that instead of having my_numbers =
[x for x in [1, 2, 3, 4, 5]]
for example, this is a list comprehension.
my_numbers_gen(), you can do a generator comprehension.
This instance is not very useful,
but this is now a generator object as well.
Which is just a shorthand for iterating over a sequence.
You can do next(my_numbers_gen).
You can print it out.
So generator comprehension, a pretty popular thing
to do as well, nor is the one printed out there.
So just wanted to mention that.
This is not the tuple comprehension;
this is a generator comprehension.
And we can use it to create a generator object on the fly,
lets us call __next__() in this syntax as well.
Again, all it does is go over this list
and yield each number as you call __next__().
So it doesn't copy the entire list, which this does.
This copies the list and give you another copy.
This let's you go over it one-by-one
without copying the entire list.
Of course this example is not very good.
You could just iterate over this list
since you've already got it there, but it can be useful
in other cases like when you're iterating
over another generator and you wanna
make some changes to it on the fly.
So it can be useful in come cases.
We will encounter more examples of this later on.
I just wanted to give you a bit of the syntax.
Anyway, that's it for this video.
Don't wanna keep you any longer.
I'll see you on the next one.