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
| Concept | Outil | Usage |
|---|---|---|
| Generics | [T], TypeVar | Code réutilisable typé |
| Protocol | class X(Protocol) | Duck typing statique |
| Descriptors | __get__, __set__ | Attributs validés |
| Metaclasses | type.__new__ | Métaprogrammation |
| Properties | @property | Getter/setter propres |
| Enum | Enum, StrEnum | Constantes typées |
| Slots | __slots__ | Optimisation mémoire |
| Operator | __add__, __eq__ | APIs naturelles |