MFormations
Modern Python Engineering

Chapitre 2

02-Python-Avance

02-Python-Avance

Python Avancé — Cours Complet

1. Système de Typage (3.12+)

Generics — Syntaxe moderne (3.12)

# Avant 3.12
from typing import List, Dict, TypeVar, Generic

T = TypeVar("T")
class Stack(Generic[T]):
    def push(self, item: T) -> None: ...

# 3.12+ — syntaxe native
class Stack[T]:
    def push(self, item: T) -> None: ...

def first[T](items: list[T]) -> T | None:
    return items[0] if items else None

# Multiple type vars
def map_zip[A, B](a: list[A], b: list[B]) -> list[tuple[A, B]]:
    return list(zip(a, b))

TypeVar avancé

from typing import TypeVar, Generic

# TypeVar contraint
Number = TypeVar("Number", int, float)

def add(a: Number, b: Number) -> Number:
    return a + b  # only int or float

# TypeVar avec bounds
from collections.abc import Iterable

T = TypeVar("T", bound=Iterable)

def first_item(items: T) -> object:
    return next(iter(items))

# Variance
T_co = TypeVar("T_co", covariant=True)   # pour Producer
T_contra = TypeVar("T_contra", contravariant=True)  # pour Consumer

class Producer[T_co]: ...    # retourne T
class Consumer[T_contra]: ...  # accepte T

Protocol — Duck typing statique

from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    """Ce qui peut être dessiné."""
    def draw(self) -> str: ...

class Circle:
    def draw(self) -> str:
        return "Drawing circle"

class Square:
    def draw(self) -> str:
        return "Drawing square"
    def area(self) -> float:
        return 4.0

def render(obj: Drawable) -> None:
    print(obj.draw())

render(Circle())  # OK — Circle implémente draw()
render(Square())  # OK — Square implémente draw()

# isinstance check
isinstance(Circle(), Drawable)  # True — grâce à @runtime_checkable

Literal et Final

from typing import Literal, Final

# Literal — valeur exacte
def set_mode(mode: Literal["read", "write", "append"]) -> str:
    return f"Mode set to {mode}"

set_mode("read")     # OK
set_mode("delete")   # type error

# Final — constante
MAX_RETRIES: Final = 3
DEFAULT_NAME: Final[str] = "unknown"

# Union avec Literal
Status = Literal["active", "inactive", "pending"]

def process(status: Status) -> None: ...

TypedDict

from typing import TypedDict, NotRequired, ReadOnly

# TypedDict classique
class UserDict(TypedDict):
    name: str
    age: int
    email: NotRequired[str]  # optionnel (3.11+)

# Syntaxe alternative
UserDict2 = TypedDict("UserDict2", {"name": str, "age": int})

# ReadOnly (3.12+)
class Config(TypedDict):
    API_KEY: ReadOnly[str]  # ne peut pas être modifié après création
    timeout: int

# Usage
user: UserDict = {"name": "Alice", "age": 30}
user["email"] = "alice@example.com"  # OK — NotRequired

dataclass_transform (3.11+)

from typing import dataclass_transform

@dataclass_transform()
class ModelMeta(type):
    """Métaclasse qui se comporte comme @dataclass."""
    def __new__(mcs, name, bases, ns):
        cls = super().__new__(mcs, name, bases, ns)
        # Ajoute __init__, __repr__, etc. automatiquement
        return cls

class Model(metaclass=ModelMeta):
    ...

class User(Model):
    name: str
    age: int

# Les IDE comprennent le constructeur
user = User(name="Alice", age=30)

2. Descripteurs

Le protocole des descripteurs

class ValidatedAttribute:
    """Descripteur avec validation."""

    def __init__(self, validator):
        self.validator = validator
        self.data = {}

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return self.data.get(id(obj), None)

    def __set__(self, obj, value):
        if not self.validator(value):
            raise ValueError(f"Invalid value: {value}")
        self.data[id(obj)] = value

    def __delete__(self, obj):
        del self.data[id(obj)]

Descripteurs concrets

class PositiveNumber:
    """Descripteur qui n'accepte que les nombres positifs."""

    def __init__(self, default: float = 0.0) -> None:
        self.default = default

    def __set_name__(self, owner: type, name: str) -> None:
        self.name = name

    def __get__(self, obj: object | None, objtype: type | None = None) -> float:
        if obj is None:
            return self
        return obj.__dict__.get(self.name, self.default)

    def __set__(self, obj: object, value: float) -> None:
        if not isinstance(value, (int, float)):
            raise TypeError(f"{self.name} must be a number")
        if value < 0:
            raise ValueError(f"{self.name} must be positive")
        obj.__dict__[self.name] = value


class Product:
    price = PositiveNumber()
    quantity = PositiveNumber()

    def __init__(self, price: float, quantity: float) -> None:
        self.price = price
        self.quantity = quantity

# Test
p = Product(10.0, 5)
p.price = -5  # ValueError!

3. Métaclasses

Création de classes

# type() — la métaclasse par défaut
MyClass = type("MyClass", (), {"x": 1})

# Métaclasse personnalisée
class SingletonMeta(type):
    """Métaclasse pour le pattern Singleton."""
    _instances: dict[type, object] = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


class Database(metaclass=SingletonMeta):
    def __init__(self):
        self.connected = False

    def connect(self):
        self.connected = True

# Une seule instance possible
db1 = Database()
db2 = Database()
assert db1 is db2  # True

Métaclasse avec enregistrement automatique

class RegistryMeta(type):
    """Enregistre automatiquement toutes les sous-classes."""
    _registry: dict[str, type] = {}

    def __new__(mcs, name: str, bases: tuple, namespace: dict) -> type:
        cls = super().__new__(mcs, name, bases, namespace)
        if name != "BasePlugin":
            mcs._registry[name.lower()] = cls
        return cls

    @classmethod
    def get_plugin(mcs, name: str) -> type | None:
        return mcs._registry.get(name)


class BasePlugin(metaclass=RegistryMeta):
    """Plugin de base."""
    def execute(self) -> str:
        raise NotImplementedError


class PrintPlugin(BasePlugin):
    def execute(self) -> str:
        return "Printing..."

class SavePlugin(BasePlugin):
    def execute(self) -> str:
        return "Saving..."

# Découverte automatique
RegistryMeta.get_plugin("printplugin").execute()  # "Printing..."

4. Propriétés

property — getter/setter Pythonic

class Temperature:
    def __init__(self, celsius: float = 0) -> None:
        self._celsius = celsius

    @property
    def celsius(self) -> float:
        return self._celsius

    @celsius.setter
    def celsius(self, value: float) -> None:
        if value < -273.15:
            raise ValueError("Below absolute zero!")
        self._celsius = value

    @property
    def fahrenheit(self) -> float:
        return self._celsius * 9 / 5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value: float) -> None:
        self.celsius = (value - 32) * 5 / 9

t = Temperature(100)
t.fahrenheit  # 212
t.fahrenheit = 32
t.celsius     # 0

cached_property (3.8+)

from functools import cached_property
import hashlib

class Document:
    def __init__(self, content: str) -> None:
        self.content = content

    @cached_property
    def hash(self) -> str:
        """Calculé une seule fois, puis mis en cache."""
        print("Computing hash...")
        return hashlib.sha256(self.content.encode()).hexdigest()

doc = Document("hello world")
doc.hash  # calcule et cache
doc.hash  # retourne la valeur cachée

5. Énumérations

Enum, IntEnum, StrEnum

from enum import Enum, IntEnum, StrEnum, auto

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

class StatusCode(IntEnum):
    OK = 200
    NOT_FOUND = 404
    ERROR = 500

class Direction(StrEnum):
    NORTH = "N"  # StrEnum 3.11+
    SOUTH = "S"
    EAST = "E"
    WEST = "W"

# auto() — valeur automatique
class HttpMethod(StrEnum):
    GET = auto()     # "GET"
    POST = auto()    # "POST"
    PUT = auto()     # "PUT"
    DELETE = auto()  # "DELETE"

# Usage
Color(1)           # Color.RED
Color["RED"]       # Color.RED
Color.RED.value    # 1
Color.RED.name     # "RED"

# Itération
for method in HttpMethod:
    print(method)

Enum avancé

from enum import Enum, auto

class Status(Enum):
    PENDING = "pending"
    ACTIVE = "active"
    BLOCKED = "blocked"

    @property
    def is_active(self) -> bool:
        return self in (Status.PENDING, Status.ACTIVE)

    @classmethod
    def active_statuses(cls) -> set["Status"]:
        return {s for s in cls if s.is_active}

    def next(self) -> "Status":
        transitions = {
            Status.PENDING: Status.ACTIVE,
            Status.ACTIVE: Status.BLOCKED,
            Status.BLOCKED: Status.PENDING,
        }
        return transitions[self]

s = Status.PENDING
s.next()  # Status.ACTIVE

6. slots

Optimisation mémoire

import sys

class WithoutSlots:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

class WithSlots:
    __slots__ = ("x", "y")  # dictionnaire __dict__ supprimé

    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

# Comparaison mémoire
wo = WithoutSlots(1, 2)
w = WithSlots(1, 2)

sys.getsizeof(wo)  # ~56 bytes (object) + __dict__ (~120 bytes)
sys.getsizeof(w)   # ~56 bytes (pas de __dict__)

# Limitations
w.z = 3  # AttributeError! (z pas dans __slots__)

# Héritage avec slots
class Point3D(WithSlots):
    __slots__ = ("z",)  # doit inclure les slots du parent

    def __init__(self, x: int, y: int, z: int) -> None:
        super().__init__(x, y)
        self.z = z

# Ajouter __dict__ aux slots si nécessaire
class FlexibleSlots:
    __slots__ = ("x", "__dict__")  # permet les attributs dynamiques

7. Surcharge d'Opérateurs

Opérateurs arithmétiques

from __future__ import annotations

class Vector:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other: Vector) -> Vector:
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other: Vector) -> Vector:
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar: float) -> Vector:
        return Vector(self.x * scalar, self.y * scalar)

    def __rmul__(self, scalar: float) -> Vector:
        return self * scalar

    def __neg__(self) -> Vector:
        return Vector(-self.x, -self.y)

    def __abs__(self) -> float:
        return (self.x ** 2 + self.y ** 2) ** 0.5

v1 = Vector(1, 2)
v2 = Vector(3, 4)
v1 + v2       # Vector(4, 6)
v1 * 3        # Vector(3, 6)
3 * v1        # Vector(3, 6) — __rmul__
abs(v1)       # ~2.236

eq et hash

class Point:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self) -> int:
        return hash((self.x, self.y))

    def __lt__(self, other: Point) -> bool:
        return (self.x, self.y) < (other.x, other.y)

# Usage
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

p1 == p2       # True
p1 is p2       # False
len({p1, p2})  # 1 (même hash)

# Tri
sorted([p3, p1])  # [Point(1,2), Point(3,4)]

enter et exit

class Timer:
    """Context manager pour mesurer le temps."""

    def __init__(self, name: str = "block") -> None:
        self.name = name

    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.elapsed = time.perf_counter() - self.start
        print(f"{self.name}: {self.elapsed:.4f}s")
        return False  # ne pas supprimer les exceptions

with Timer("computation"):
    sum(range(10_000_000))

8. Patterns et Pratiques

Pattern Factory

from typing import Protocol

class NotificationService(Protocol):
    def send(self, message: str) -> str: ...

class EmailService:
    def send(self, message: str) -> str:
        return f"Email: {message}"

class SMSService:
    def send(self, message: str) -> str:
        return f"SMS: {message}"

class NotificationFactory:
    _services: dict[str, type[NotificationService]] = {}

    @classmethod
    def register(cls, name: str, service: type[NotificationService]) -> None:
        cls._services[name] = service

    @classmethod
    def create(cls, name: str) -> NotificationService:
        service = cls._services.get(name)
        if not service:
            raise ValueError(f"Unknown service: {name}")
        return service()

NotificationFactory.register("email", EmailService)
NotificationFactory.register("sms", SMSService)

Pattern Repository

from typing import Protocol, Generic, TypeVar
from dataclasses import dataclass

T = TypeVar("T")

@dataclass
class User:
    id: int
    name: str
    email: str

class Repository(Protocol[T]):
    def get(self, id: int) -> T | None: ...
    def save(self, entity: T) -> T: ...
    def delete(self, id: int) -> bool: ...
    def find_all(self) -> list[T]: ...

class InMemoryUserRepository:
    def __init__(self) -> None:
        self._users: dict[int, User] = {}
        self._next_id = 1

    def get(self, id: int) -> User | None:
        return self._users.get(id)

    def save(self, user: User) -> User:
        if user.id == 0:
            user.id = self._next_id
            self._next_id += 1
        self._users[user.id] = user
        return user

    def delete(self, id: int) -> bool:
        return self._users.pop(id, None) is not None

    def find_all(self) -> list[User]:
        return list(self._users.values())

9. Tableau Récapitulatif

ConceptOutilUsage
Generics[T], TypeVarCode réutilisable typé
Protocolclass X(Protocol)Duck typing statique
Descriptors__get__, __set__Attributs validés
Metaclassestype.__new__Métaprogrammation
Properties@propertyGetter/setter propres
EnumEnum, StrEnumConstantes typées
Slots__slots__Optimisation mémoire
Operator__add__, __eq__APIs naturelles