Python
  • Intro.
  • Catalogue
  • Chapter 1: Introduction to Python
  • Chapter 2: Python Syntax and Fundamentals
    • Chapter: Variables and Data Types in Python
  • Chapter 3: Control Flow
  • Chapter 4: Functions
  • Chapter 5: Data Structures
  • Chapter 6: Object-Oriented Programming (OOP)
  • Chapter 7: Modules and Packages
  • Chapter 8: File Handling
  • Chapter 9: Error and Exception Handling
  • Chapter 10: Working with Databases
  • Chapter 11: Iterators and Generators
  • Chapter 12: Decorators and Context Managers
  • Chapter 13: Concurrency and Parallelism
  • Chapter 14: Testing and Debugging
  • Chapter 15: Web Development with Python
  • Chapter 16: Data Science and Machine Learning with Python
  • Chapter 17: Working with APIs
  • Chapter 18: Automation with Python
  • Chapter 19: Python and Cloud/DevOps
  • Chapter 20: Python and IoT
  • Appendices
Powered by GitBook
On this page

Chapter 12: Decorators and Context Managers

Decorators and context managers are advanced features in Python that enhance code functionality and manage resources effectively. Decorators allow you to modify or extend the behavior of functions or methods, while context managers help manage resources, such as file handling, with clean and concise syntax.

Decorators

A decorator is a function that takes another function as input and extends or modifies its behavior without changing its structure.

Anatomy of a Decorator:

  1. Define a decorator function.

  2. Accept a function as an argument.

  3. Define a wrapper function inside the decorator.

  4. Return the wrapper function.

Example:

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

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

say_hello()
# Output:
# Before the function call
# Hello!
# After the function call

Using Decorators with Arguments:

def repeat_decorator(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat_decorator(3)
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

Built-in Decorators

Python provides several built-in decorators such as @staticmethod, @classmethod, and @property.

Example:

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value

circle = Circle(5)
print(circle.radius)  # Output: 5
circle.radius = 10
print(circle.radius)  # Output: 10

Context Managers

Context managers handle resource management tasks such as opening and closing files, acquiring and releasing locks, etc. The most common example is the with statement.

Example with with:

with open("example.txt", "r") as file:
    content = file.read()
    print(content)
# File is automatically closed after the block

Creating Custom Context Managers

Custom context managers can be created using the __enter__() and __exit__() methods in a class.

Example:

class MyContext:
    def __enter__(self):
        print("Entering the context")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("Exiting the context")

with MyContext() as context:
    print("Inside the context")
# Output:
# Entering the context
# Inside the context
# Exiting the context

Contextlib Module

The contextlib module provides utilities for creating and working with context managers.

Example with contextlib:

from contextlib import contextmanager

@contextmanager
def my_context():
    print("Entering the context")
    yield
    print("Exiting the context")

with my_context():
    print("Inside the context")
# Output:
# Entering the context
# Inside the context
# Exiting the context

Combining Decorators and Context Managers

You can use both features together to create clean, efficient, and readable code.

Example:

from contextlib import contextmanager

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}...")
        result = func(*args, **kwargs)
        print(f"Finished {func.__name__}.")
        return result
    return wrapper

@log_decorator
@contextmanager
def managed_resource():
    print("Resource acquired")
    yield
    print("Resource released")

with managed_resource():
    print("Using the resource")
# Output:
# Calling managed_resource...
# Resource acquired
# Using the resource
# Resource released
# Finished managed_resource.

Exercises

Exercise 1:

Write a decorator that logs the execution time of a function.

Solution:

import time

def execution_time(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Execution time: {end_time - start_time:.2f} seconds")
        return result
    return wrapper

@execution_time
def slow_function():
    time.sleep(2)
    print("Finished work")

slow_function()
# Output:
# Finished work
# Execution time: 2.00 seconds

Exercise 2:

Create a context manager that suppresses specific exceptions.

Solution:

from contextlib import contextmanager

@contextmanager
def suppress_exception(exc_type):
    try:
        yield
    except exc_type:
        print(f"Suppressed {exc_type.__name__}")

with suppress_exception(ZeroDivisionError):
    print(10 / 0)  # Output: Suppressed ZeroDivisionError

Exercise 3:

Create a custom context manager to log the entry and exit of a block.

Solution:

class LogContext:
    def __enter__(self):
        print("Entering the block")

    def __exit__(self, exc_type, exc_value, traceback):
        print("Exiting the block")

with LogContext():
    print("Inside the block")
# Output:
# Entering the block
# Inside the block
# Exiting the block

Best Practices

  1. Use decorators for reusable and modular functionality.

  2. Prefer contextlib for simple context managers.

  3. Test custom context managers and decorators for edge cases.

  4. Keep wrapper functions in decorators lightweight.

  5. Use meaningful names for decorators and context manager classes.

In the next chapter, we will explore concurrency and parallelism, focusing on multithreading, multiprocessing, and asynchronous programming in Python.

PreviousChapter 11: Iterators and GeneratorsNextChapter 13: Concurrency and Parallelism

Last updated 5 months ago