← Back to Tutorials Chapter 17

Advanced Concepts

Python advanced concepts diagram

Metaclasses

A metaclass is the class of a class. Just as a class defines how instances behave, a metaclass defines how classes behave. The default metaclass is type.

class Meta(type):
    def __new__(cls, name, bases, dct):
        dct["version"] = 1.0
        return super().__new__(cls, name, bases, dct)

class MyClass(metaclass=Meta):
    pass

print(MyClass.version)  # 1.0

You can also create classes dynamically using type:

DynamicClass = type("DynamicClass", (), {"x": 10})
obj = DynamicClass()
print(obj.x)

Context Managers

Context managers manage resources using the with statement. Implement __enter__ and __exit__ methods, or use contextlib.contextmanager.

class File:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.file.close()

with File("test.txt", "w") as f:
    f.write("Hello, context manager!")

# Using contextlib
from contextlib import contextmanager

@contextmanager
def temporary_file(name):
    f = open(name, "w")
    try:
        yield f
    finally:
        f.close()

Coroutines and Async/Await

Asynchronous programming in Python uses async def and await with the asyncio library for concurrent I/O.

import asyncio

async def fetch_data(url):
    print(f"Fetching {url}...")
    await asyncio.sleep(1)  # Simulate network I/O
    return f"Data from {url}"

async def main():
    urls = ["url1", "url2", "url3"]
    tasks = [fetch_data(url) for url in urls]
    results = await asyncio.gather(*tasks)
    return results

results = asyncio.run(main())
print(results)

Descriptors

Descriptors are objects that define __get__, __set__, or __delete__ methods. They control attribute access on classes.

class PositiveNumber:
    def __get__(self, obj, objtype=None):
        return obj.__dict__.get(self.name, 0)

    def __set__(self, obj, value):
        if value < 0:
            raise ValueError("Must be positive")
        obj.__dict__[self.name] = value

    def __set_name__(self, owner, name):
        self.name = f"_{name}"

class Order:
    quantity = PositiveNumber()

order = Order()
order.quantity = 10
print(order.quantity)

Memory Management

Python uses reference counting and a generational garbage collector (gc module). Objects are freed when their reference count reaches zero.

import gc

gc.enable()
print(gc.get_count())

# Circular references
class Node:
    def __init__(self):
        self.ref = None

a = Node()
b = Node()
a.ref = b
b.ref = a

del a, b
gc.collect()  # Force garbage collection

Type Hints (typing Module)

from typing import List, Dict, Optional, Tuple, Union

def process_items(items: List[str]) -> Dict[str, int]:
    result: Dict[str, int] = {}
    for item in items:
        result[item] = len(item)
    return result

def find_user(user_id: int) -> Optional[Dict]:
    # Returns None if not found
    pass

def combine(a: Union[int, str], b: Union[int, str]) -> str:
    return str(a) + str(b)

Monkey Patching

Monkey patching dynamically modifies classes or modules at runtime. Use it sparingly as it can make code harder to debug.

class Dog:
    def bark(self):
        return "Woof!"

def howl(self):
    return "Howl!"

Dog.howl = howl
d = Dog()
print(d.howl())  # Howl!

# Patching a module
import math
math.original_sqrt = math.sqrt
math.sqrt = lambda x: math.original_sqrt(x) if x >= 0 else 0
Note: Advanced features like metaclasses and monkey patching should be used only when simpler solutions are insufficient. They add complexity and can make maintenance difficult.
Exercise: Implement a context manager that measures the execution time of a block of code and logs it. Write an async function that fetches 5 URLs concurrently using asyncio.gather and measures total time. Then create a custom descriptor that validates that a value is within a specified range.