Unlocking Pythons Hidden Powers Deep Dive Into Special Methods Part 2
In part 1, I started by demonstrating a use case for the __iter__()
iterator special (magic) methods. I gave an example of how it can be used in a StudentCollection
class. I also highlighted the fact that adopting __iter__
leads to more Pythonic code and better utilisation of Python’s language features.
In part 2, I will be writing about the iterator.__next__()
special method, which usually gets combined with the iterator.__iter__()
method. As part of my reading and preparing to write this piece, I discovered there is a thing called iterator protocol in the Python docs, and a proper description of how to write iterators. I had looked at the code in part 1 and thought it worked, because without knowing I implemented the container.__iter__()
approach which is called Iterables.
In light of my new found knowledge, I intend to re-write the StudentCollection
class, to adhere to the iterator protocol and in so doing introduce the __next__()
method.
Recap of the StudentCollection
class.
class StudentCollection:
def __init__(self, students: list[str] | None = None):
self._students = students
def add_student(self, student: str):
if self._students is None:
self._students = []
self._students.append(student)
# we can remove the list_all_students() method.
def __iter__(self) -> Generator[str | None, None, None]:
if self._students:
for student in self._students:
yield student
What is the iterator.__next__()
special method?
The __next__()
method is used to return the next item from an iterator. If there are no items to return, a StopIteration
exception is raised. A lot easier to demonstrate in code. I will be updating our container class StudentCollection
class StudentCollection:
def __init__(self, students: list[str]):
self._students = students
self.index = 0
self.current = None
def __iter__(self) -> object:
return self
def __next__(self):
if self.index < len(self._students):
self.current = self._students[self.index]
self.index += 1
return self.current
raise StopIteration
The StudentCollection container/class has been updated to hold two new properties - index and current. You don’t necessarily need the current
property but it can be useful to keep a track of the currently returned student. This allows you to write print(StudentCollection.current)
to fetch the last or current student at the index. Without the current
property, you will need to keep track of the last returned item in your calling code.
The index
property on the other hand, is useful for tracking the currently selected or viewed student in the self.students
collection. Incrementing the index
value, points to the next student in the list. While the value of index
is less than the length of the student list, we can still have students to return when asked for. Once that condition doesn’t hold, by the iterator protocol, we should raise a StopIteration exception.
Running the code.
students = StudentCollection(['st1', 'st2', 'st3', 'st4'])
for student in students:
print('index ', students.index, ' student ', student)
# Output:
# index 1 student st1
# index 2 student st2
# index 3 student st3
# index 4 student st4
An alternative approach to looping over the StudentCollection
is with a while loop. A while loop however throws up one additional requirement, when we run out of students to iterate over, a StopIteration
exception is raised and this will need handling in your calling code. This requirement is handled for you when using a for in
construct.
students = StudentCollection(['st1', 'st2', 'st3', 'st4'])
try:
while student := next(students):
print('index ', students. index, ' student ', student)
except StopIteration:
print('finish!!')
# Output:
# index 1 student st1
# index 2 student st2
# index 3 student st3
# index 4 student st4
# finish!!
In conclusion, the __next__()
method is a critical part of Python’s iterator protocol. It allows for more efficient and idiomatic iteration over collections, as demonstrated with the StudentCollection
class. The __next__()
method, along with the __iter__()
method, enables Python objects to be used directly in loops and other constructs that expect an iterator. This functionality demonstrates Python’s powerful and flexible design, and underscores the importance of understanding these special methods for effective Python programming.