# Functions: Iterables and Iterators

alanhrosenthal

Very complete. Very easy to understand.

Well Done!

Mahesh

getting below error for ZIP.py <zip object at 0x00633F80>

peterabbeel

Related to above comment:

getting below error for ZIP.py <zip object at 0x00633F80>.

Note this is not an error.

`print(merged_2)` returns the memory address of the iterator `merged_2` `print(list(merged_2))` returns the content of the iterator `merged_2`

You can also call the zip functionality as following:

``````print(list(zip(countries, continents)))
``````

Cory

Exactly what peterabbeel did on <zip object at 0x036B4F08> I also had to switch mine to print(tuple(merged_2) which then gave me the proper output although it makes it immutable. Still learned a ton from this tutorial.

Alain Rouleau

I didn’t even know some of these functions existed such as `any()` and `all()`. Of course everybody should already know about them since their just built-in functions. But very handy.

Learned quite a lot from this video, thanks.

leroygcooper

Hey Darren. In the zip function example using the “merged” for-loop, were there two sets of brackets around the “merged.append…” line due to the argument being a tuple (countries[i], continents[i])?

Bartosz Zaczyński RP Team

@leroygcooper That’s correct. You need to wrap both elements in a tuple because the list’s `.append()` method expects only a single item, and we want the list to contain pairs.

catchkarthik

Can you give me an actual use case where function `reversed()` can be used? I understand that python stores it as an object and we have to feed it to a list to retrieve the values. Is there a use case where reversed can be used without a list function? may be in a loop function where i need to print the items in a list, in a reverse order?

Bartosz Zaczyński RP Team

@catchkarthik The `reversed()` function accepts any iterable object, while the `list.reverse()` method is only defined for lists. Additionally, sequences like lists, tuples, or strings support the slicing syntax, e.g., `text[::-1]`, which lets you achieve a similar effect by making a reversed copy of the original sequence. On the other hand, `reversed()` is lazily evaluated and creates a lightweight view of the original data without allocating new memory. This makes changes to the original data visible in your reversed view. You’ll find much more details about the differences between those methods for reversing sequences in the written tutorial: realpython.com/python-reverse-list/

You’d use `reversed()` in a loop or a comprehension expression, like you said. There’s also a protocol you can leverage in your custom classes:

``````>>> class CustomClass:
...     def __iter__(self):
...         yield from [1, 2, 3]
...
...     def __reversed__(self):
...         yield from [3, 2, 1]

>>> instance = CustomClass()
>>> for value in instance:
...     print(value)
...
1
2
3

>>> for value in reversed(instance):
...     print(value)
...
3
2
1
``````

aquibmir

So what exactly is the difference between an Iterable and an Iterator? How do we define them?

``````e = reversed(d)
``````

is the Iterator and

``````list(e)
``````

is the Iterable?

Bartosz Zaczyński RP Team

@aquibmir See the Python Glossary for the relevant definitions:

In a nutshell, an iterable is a composite object able to yield its elements in a for-loop. There are all kinds of iterable objects in Python, including strings, tuples, lists, dictionaries, and so on. On the other hand, objects like integers aren’t iterable because you can’t iterate over them in a loop.

An iterator is another type of object whose role is to hide the details of visiting the individual elements of an iterable. It’s straightforward to think of iterating over a sequence like a Python list because each element has an associated index. However, some iterables don’t keep their elements in any particular order. For example, sets, trees, graphs, and other data structures are organized differently in memory, yet they all behave the same when you iterate over them.

You can define your own iterable that hooks into the for-loop by implementing a few special methods in a custom class. If your iterable has a known length and is a sequence indexed by a numeric index or a mapping accessed by a hashable key, then you can implement these two special methods in your custom class:

``````class Iterable:
def __getitem__(self, key_or_index):
...
def __len__(self):
...
``````

Otherwise, you can use a more generic iterator protocol by implementing the `.__iter__()` special method instead:

``````class Iterable:
def __iter__(self):
return Iterator(...)
``````

Moreover, if you’d like to support iteration in reversed order, i.e., when someone calls `reversed()` on your iterable, then you can optionally add yet another special method:

``````class Iterable:
def __iter__(self):
return Iterator(...)
def __reversed__(self):
return ReversedIterator(...)
``````

In both cases, you return a new instance of an iterator class, which will handle the iteration. Alternatively, when the iteration has a relatively simple logic, it’s customary to use a generator iterator instead of returning a new instance of a class:

``````class Iterable1:
def __iter__(self):
yield from self.items

class Iterable2:
def __iter__(self):
for item in self.items:
yield item
``````

If you choose to define an iterator class, then it must implement at least the `.__next__()` special method, which supplies the next element of the iterable. However, to comply with the iterator protocol, it should also implement the `.__iter__()` method that returns itself:

``````class Iterator:
def __next__(self):
...
def __iter__(self):
return self
``````

This makes the iterator iterable itself, which is mandatory if you want to use it with a for-loop, which always calls `iter()` first, and then keeps calling `next()` on the resulting iterator object. To test your iterable and the corresponding iterator, you can simulate a for-loop by calling these functions manually:

``````>>> class Iterable:
...     def __init__(self, *elements):
...         self.elements = elements
...     def __iter__(self):
...         return Iterator(self.elements)

>>> class Iterator:
...     def __init__(self, elements):
...         self.elements = elements
...         self.index = -1
...     def __next__(self):
...         self.index += 1
...         if self.index < len(self.elements):
...             return self.elements[self.index]
...         raise StopIteration
...     def __iter__(self):
...         return self

>>> iterable = Iterable("apple", "banana", "orange")
>>> iterator = iter(iterable)
>>> next(iterator)
'apple'
>>> next(iterator)
'banana'
>>> next(iterator)
'orange'
>>> next(iterator)
Traceback (most recent call last):
File "<input>", line 1, in <module>
next(iterator)
File "<input>", line 9, in __next__
raise StopIteration
StopIteration
``````

To signal that all elements have been exhausted, an iterator must raise the `StopIteration` exception. However, it’s optional since your iterator might be infinite, in which case it will never exhaust iterable’s elements.

Note that iterators are typically one-use only, which means that once they reach the last element in the iterable, they’re no longer useful. You must create a brand new iterator instance, which will point the first element in an iterable.

to join the conversation.

Lesson Completed!
Lesson Bookmarked
Request Failed :(