TUTORIAL SERIES

Design Patterns in Python: Iterator

Navigating Collections

Amir Lavasani
6 min readDec 18, 2023

Have you encountered recurring coding challenges? Imagine having a toolbox of tried-and-true solutions readily available. That’s precisely what design patterns provide. In this series, we’ll explore what these patterns are and how they can elevate your coding skills.

Understanding the Iterator Pattern

What is the Iterator Design Pattern?

The Iterator Design Pattern is a behavioral pattern, it helps loop through collections without showing how they’re built inside. It separates the way you move through a group of things from the actual group.

This makes code cleaner and lets you iterate through different collections in the same way.

When to Use the Iterator Pattern

The Iterator Pattern simplifies collection traversal by separating how we move through things from their internal structure. Useful in:

  1. Looping Through Collections: When traversing lists, arrays, or collections without knowledge of their internal structure.
  2. Hiding Complexity: Concealing intricate data storage details for easier access and manipulation.
  3. Custom Iteration Logic: Creating personalized iteration methods for custom data structures.
  4. Standardized Access: Ensuring uniform traversal methods across diverse collections.
Dall-E generated image with the following concept: Infinity symbol loop indicating the continuous and cyclical nature of iterations

Terminology and Key Components

Understanding the Iterator Pattern involves recognizing its key components that enable smooth traversal through collections. Here are the essential components:

  1. Iterator: Declares operations like fetching the next element, retrieving the current position, and restarting iteration.
  2. Concrete Iterator: Implement specific traversal algorithms, enabling independent traversal progress for multiple iterators.
  3. Collection: Declares methods for obtaining iterators compatible with the collection, ensuring varied iterator types.
  4. Concrete Collections: Generate specific iterator instances upon request, encapsulating collection details within the same class.
Iterator design pattern structure diagram. Image from refactoring.guru

Python and Iterator Pattern

Python integrates the Iterator Pattern into its core syntax, embedding fundamental iteration principles within the language’s design.

However, Python simplifies the iteration protocol by employing dunder methods such as __iter__() and __next__(), which form the backbone of the Iterator Pattern.

Instead of directly calling these methods, Python provides two built-in functions, iter() and next(), as the preferred means to interact with these dunder methods.

Python’s for Loop: Simplifying Iteration

Python’s for loop conceals the Iterator Pattern, handling sequential assignments for loop sequences. It executes code blocks per assignment, supports control statements like break and continue, and allows unpacking multiple elements.

Its flexibility extends to efficient dictionary operations using methods like items(). Additionally, Python's concise nature integrates the for loop into expressions—comprehensions—simplifying the direct creation of lists, sets, dictionaries, and generators from inline loops.

This design simplifies Python, enhancing its intuitiveness and ease of learning by reusing the familiar for statement syntax across various contexts.

Iterator Pattern in Python with Custom Objects

In this code, MyIterator defines __iter__() to return the object itself as an iterator.

The __next__() method controls the iteration process by sequentially accessing elements from the data attribute of the object. When the for loop iterates over iter_obj, it implicitly calls the __iter__() method to obtain an iterator and uses __next__() to retrieve elements until StopIteration is raised, indicating the end of the iteration.

class MyIterator:
def __init__(self, data):
self.data = data
self.index = 0

def __iter__(self):
return self # Returns itself as an iterator

def __next__(self):
if self.index >= len(self.data):
raise StopIteration # Raises StopIteration when iteration is complete
value = self.data[self.index]
self.index += 1
return value

# Usage Example
my_list = [1, 2, 3, 4, 5]
iter_obj = MyIterator(my_list)

# Using the for loop to iterate through the object
for element in iter_obj:
print(element)

Iterator Pattern Implementation in Python

In this section, we implement the classic Iterator pattern using Python.

Step 1: Iterator Interface

Define the Iterator interface specifying the methods required for traversal.

class Iterator:
"""Step 1: Create the Iterator interface."""

def __iter__(self):
"""Defines the __iter__() method to return self as an iterator."""
raise NotImplementedError

def __next__(self):
"""Defines the __next__() method to retrieve elements sequentially."""
raise NotImplementedError

Step 2: Concrete Iterator

Implement a Concrete Iterator, managing the iteration process.

class MyIterator(Iterator):
"""Step 2: Implement a Concrete Iterator."""

def __init__(self, data):
self.data = data
self.index = 0

def __iter__(self):
return self # Returning self as an iterator

def __next__(self):
if self.index >= len(self.data):
raise StopIteration
value = self.data[self.index]
self.index += 1
return value

Step 3: Collection

Create the Collection interface to declare methods for creating iterators.

class Collection:
"""Step 3: Create the Collection interface."""

def create_iterator(self):
"""Method to create an Iterator compatible with the collection."""
raise NotImplementedError

Step 4: Concrete Collections

Implement a Concrete Collection defining the collection and creating iterators.

class MyCollection(Collection):
"""Step 4: Implement a Concrete Collection."""

def __init__(self):
self.data = []

def add(self, value):
self.data.append(value)

def create_iterator(self):
return MyIterator(self.data)

Client Code

Demonstrate the usage of the implemented Iterator Pattern.

def main():
"""Demonstrate the usage of the implemented Iterator Pattern."""

# Creating a collection
my_collection = MyCollection()
my_collection.add(1)
my_collection.add(2)
my_collection.add(3)

# Creating an iterator for the collection
my_iterator = my_collection.create_iterator()

# Using the iterator to traverse through the elements
for element in my_iterator:
print(element)


if __name__ == "__main__":
main()

GitHub Repo 🎉

Explore all code examples and design pattern implementations on GitHub!

Best Practices and Considerations

When implementing the Iterator pattern, it’s essential to consider both its advantages and potential drawbacks:

Advantages

  • Flexibility: Offers a standardized way to access elements without exposing the underlying structure, enhancing code flexibility and maintainability.
  • Seamless Traversal: Simplifies iteration across diverse collections, enabling consistent traversal logic across various data structures.
  • Encapsulated Iteration Logic: Isolates iteration logic from the collection, promoting cleaner and more readable code by separating concerns.

Considerations

  • Complexity: Introducing iterators might add complexity to simple collection traversals, potentially overcomplicating code where direct iteration suffices.
  • Performance Overhead: In certain scenarios, the additional abstraction of iterators might introduce a slight performance overhead compared to direct iteration methods.
  • Learning Curve: For developers new to the pattern, understanding and implementing iterators might require additional learning and adjustment time.
Dall-E generated image with the following concept: An abstract representation of linked chains, each link signifying an iteration, showcasing continuity and flow

Relations with Other Patterns — TL;DR;

Comparing the Iterator Pattern to other design patterns sheds light on their collaborative potential:

Iterator and Composite

Iterators effectively traverse Composite trees, enabling efficient access to and manipulation of complex hierarchical structures.

Iterator and Factory

Coupling the Factory Method with an Iterator allows different collection subclasses to yield various types of iterators compatible with the collections, enhancing flexibility in iteration strategies.

Iterator and Memento Pattern

Combining Memento with Iterator captures the present iteration state, enabling rollback capabilities if necessary, and facilitating a snapshot-based iteration control mechanism.

Iterator and Visitor Pattern

When used alongside Iterator, Visitor facilitates traversal of intricate data structures, executing operations across diverse elements, regardless of their individual classes, empowering versatile element-wise operations.

Conclusion

Our journey through this article explored the Iterator pattern deeply. From its core definition to its Python implementation, we’ve uncovered its role in powering the for loop and crafting custom iterators.

Additionally, we’ve glimpsed its connections with other design patterns, highlighting its versatile nature within Python’s structure. This exploration offers a clearer understanding of how the Iterator pattern simplifies data traversal and collaborates across different design patterns in Python.

Hope you enjoyed the Iterator pattern exploration 🙌 Happy coding! 👨‍💻

Next on the Series 🚀

Read More 📜

The Series 🧭

References

  1. Design Patterns: Elements of Reusable Object-Oriented Software (Book)
  2. refactoring.guru Iterator
  3. Head First Design Patterns (Book)
  4. python-patterns.guide The Iterator Pattern
  5. packtpub The Iterator Pattern
  6. Iterators and Iterables in Python

--

--

Amir Lavasani

I delve into machine learning 🤖 and software architecture 🏰 to enhance my expertise while sharing insights with Medium readers. 📃