Iterables and Containers
Make your objects work with `for` loops and the `in` operator.
Python allows you to make your objects iterable and work as containers, enabling them to work seamlessly with for loops, comprehensions, and membership testing.
Iterable Objects
Section titled “Iterable Objects”An object is iterable if it implements __iter__(), which returns an iterator. The iterator must implement __next__() to return the next item.
Basic Iterable
Section titled “Basic Iterable”from datetime import datetime, timedelta
class DateRange: """Custom iterable for date ranges""" def __init__(self, start_date: datetime, end_date: datetime): self.start_date = start_date self.end_date = end_date
def __iter__(self): """Returns iterator object""" self.current_date = self.start_date return self
def __next__(self): """Returns next item in sequence""" if self.current_date > self.end_date: raise StopIteration date_to_return = self.current_date self.current_date += timedelta(days=1) return date_to_return
# Usagefor date in DateRange(datetime(2023, 1, 1), datetime(2023, 1, 5)): print(date.strftime("%Y-%m-%d"))
# Output:# 2023-01-01# 2023-01-02# 2023-01-03# 2023-01-04# 2023-01-05Real-World Example: Paginated API Results
Section titled “Real-World Example: Paginated API Results”class PaginatedResults: """Iterable for paginated API responses""" def __init__(self, fetch_page_func, total_pages: int): self.fetch_page = fetch_page_func self.total_pages = total_pages
def __iter__(self): self.current_page = 1 return self
def __next__(self): if self.current_page > self.total_pages: raise StopIteration page_data = self.fetch_page(self.current_page) self.current_page += 1 return page_data
# Usagedef fetch_api_page(page_num): return [f"Item {page_num}-{i}" for i in range(5)]
results = PaginatedResults(fetch_api_page, total_pages=3)for page in results: print(page)Container Objects
Section titled “Container Objects”Containers implement __contains__() for membership testing with the in keyword. They often also implement __len__() and __getitem__().
How in Works
Section titled “How in Works”element in containerThis translates to:
container.__contains__(element)Custom Container Example
Section titled “Custom Container Example”class Boundaries: """Container that checks if a point is within boundaries""" def __init__(self, width: float, height: float): self.width = width self.height = height
def __contains__(self, point: tuple) -> bool: """Check if point (x, y) is within boundaries""" x, y = point return 0 <= x <= self.width and 0 <= y <= self.height
class Grid: """Grid that uses Boundaries via composition""" def __init__(self, width: float, height: float): self.width = width self.height = height self.limits = Boundaries(width, height) # Composition
def __contains__(self, point: tuple) -> bool: """Delegate to Boundaries""" return point in self.limits
# Usagegrid = Grid(10, 10)print((5, 5) in grid) # Trueprint((15, 5) in grid) # Falseprint((5, 15) in grid) # FalseclassDiagram
class Grid {
-width: float
-height: float
-limits: Boundaries
+__contains__(point: tuple) bool
}
class Boundaries {
-width: float
-height: float
+__contains__(point: tuple) bool
}
Grid *-- Boundaries : composition
Combining Iterable and Container
Section titled “Combining Iterable and Container”You can make objects both iterable and containers:
class NumberRange: """Range that is both iterable and a container""" def __init__(self, start: int, end: int): self.start = start self.end = end
def __iter__(self): """Make it iterable""" self.current = self.start return self
def __next__(self): """Next value in iteration""" if self.current > self.end: raise StopIteration value = self.current self.current += 1 return value
def __contains__(self, value: int) -> bool: """Make it a container""" return self.start <= value <= self.end
def __len__(self) -> int: """Length of the range""" return max(0, self.end - self.start + 1)
# Usagerange_obj = NumberRange(1, 10)
# Iterable - works with for loopsfor num in range_obj: print(num, end=" ") # 1 2 3 4 5 6 7 8 9 10
# Container - works with 'in'print(5 in range_obj) # Trueprint(15 in range_obj) # False
# Lengthprint(len(range_obj)) # 10Real-World Example: Custom Collection
Section titled “Real-World Example: Custom Collection”class TaggedItems: """Collection that supports iteration and membership testing""" def __init__(self): self.items = [] self.tags = {}
def add_item(self, item: str, tags: list[str]): """Add item with tags""" self.items.append(item) self.tags[item] = tags
def __iter__(self): """Iterate over items""" return iter(self.items)
def __contains__(self, item: str) -> bool: """Check if item exists""" return item in self.items
def __len__(self) -> int: """Number of items""" return len(self.items)
def items_with_tag(self, tag: str): """Generator for items with specific tag""" for item in self.items: if tag in self.tags.get(item, []): yield item
# Usagecollection = TaggedItems()collection.add_item("Python", ["programming", "language"])collection.add_item("JavaScript", ["programming", "web"])collection.add_item("SQL", ["database", "query"])
# Iterablefor item in collection: print(item)
# Containerprint("Python" in collection) # Trueprint("Ruby" in collection) # False
# Lengthprint(len(collection)) # 3
# Custom iterationfor item in collection.items_with_tag("programming"): print(item) # Python, JavaScript