Python Iterators and Decorators

In Python, two advanced yet essential concepts are Iterators and Decorators. Mastering these topics will allow you to write cleaner, efficient, and more powerful code.

This tutorial will cover:

  • What are Iterators?
  • Implementing custom Iterators.
  • Iterator protocols (iter() and next()).
  • What are Decorators?
  • Writing and applying custom Decorators.
  • Practical examples and explanations.

Let’s dive into each in detail!

Iterators in Python

What is an Iterator?

An iterator is an object in Python that allows a programmer to traverse through all elements of a collection (e.g., lists, tuples, dictionaries, or strings). An iterator maintains a state to remember its current position during iteration.

Iterable vs Iterator
  • Iterable: Any object you can loop over (e.g., lists, tuples).
  • Iterator: An object that provides a __next__() method to retrieve the next item and a __iter__() method.
Iterator Protocol

An object must implement two methods to become an iterator:

  • __iter__(): returns the iterator object itself.
  • __next__(): returns the next element.

Example of a Custom Iterator:

Let’s create an iterator class that generates a sequence of numbers:

class MyIterator:
    def __init__(self, max_value):
        self.max_value = max_value
        self.current = 0

    def __iter__(self):
        # Returns the iterator object itself
        return self

    def __next__(self):
        # Defines how to get the next element
        if self.current < self.max_value:
            self.current += 1
            return self.current
        else:
            # Stops iteration
            raise StopIteration

Using the Iterator:

numbers = MyIterator(5)

for num in numbers:
    print(num)

Explanation of the Example:

  • The class MyIterator implements both __iter__() and __next__().
  • Each call to __next__() increments self.current and returns it until reaching max_value.
  • Once reached, the iterator raises a StopIteration exception automatically handled by Python’s for loop.

Output:

Decorators in Python

What is a Decorator?

A decorator in Python is a design pattern that allows you to add extra functionality to an existing function without modifying its original structure. Decorators are very powerful, enabling cleaner and more readable code.

Why Use Decorators?
  • Code reusability
  • Cleaner syntax and code maintenance
  • Cross-cutting concerns (logging, timing, access control, etc.)

Basic Decorator Syntax in Code:

def my_decorator(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()

Explanation of Basic Decorator:

  • my_decorator wraps the original say_hello function.
  • The decorator adds extra statements before and after the function call.
  • @my_decorator syntax simplifies the application of the decorator.

Output:

Detailed Example: Practical Decorator (Timing and Logging)

Here’s a more advanced example: a decorator that measures the execution time of a function.

import time

def timer_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Function '{func.__name__}' executed in {end_time - start_time:.5f} seconds")
        return result
    return wrapper

@timer_decorator
def calculate_squares(n):
    return [i ** 2 for i in range(n)]

calculate_squares(10000)

Explanation of Timer Decorator:

  • The decorator timer_decorator records the time before and after the function call.
  • *args and **kwargs allow the decorated function to accept arbitrary arguments.
  • The wrapper prints out how long the function took to execute.

Output (Example):

Chaining Multiple Decorators

You can apply multiple decorators to a single function. The decorators execute from the bottom up (nearest to the function first).

def uppercase_decorator(func):
    def wrapper():
        return func().upper()
    return wrapper

def bold_decorator(func):
    def wrapper():
        return f"<b>{func()}</b>"
    return wrapper

@bold_decorator
@uppercase_decorator
def greet():
    return "hello decorators!"

print(greet())

Explanation of Decorator Chaining

  • greet() is first modified by uppercase_decorator, turning the returned text uppercase.
  • Then bold_decorator wraps the uppercase text with <b> tags.
  • Order of decorators matters significantly.

Output:

Iterators vs Decorators: When to Use Which?

  • Use iterators when you need custom traversal logic, lazy evaluation, or memory-efficient looping (for instance, when handling large datasets).
  • Use decorators when you need reusable additional behaviors like logging, timing, authentication, or modifying function outputs without altering the original code structure.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top