Apprendre Python de zéro jusqu'au niveau expert : syntaxe, structures de données, POO, générateurs, décorateurs, typage, tests, async et internals du langage.
En Python, tout est objet : les entiers, les fonctions, les classes elles-mêmes. La programmation orientée objet consiste à regrouper des données et les comportements qui s'y appliquent dans une même unité, la classe. Python le fait avec une syntaxe légère et une philosophie pragmatique : pas de private, pas d'interfaces obligatoires, on fait confiance au développeur.
class CompteBancaire:
taux_interet = 0.02 # attribut de classe, partagé
def __init__(self, titulaire: str, solde: float = 0):
self.titulaire = titulaire # attribut d'instance
self.solde = solde
def deposer(self, montant: float) -> None:
if montant <= 0:
raise ValueError("Montant positif requis")
self.solde += montant
def __repr__(self) -> str:
return f"CompteBancaire({self.titulaire!r}, {self.solde})"
compte = CompteBancaire("Ada", 100)
compte.deposer(50)
compte # CompteBancaire('Ada', 150)self est l'instance, passée automatiquement. compte.deposer(50) équivaut à CompteBancaire.deposer(compte, 50).
_solde : « interne », accessible mais on ne devrait pas y toucher__solde : name mangling, renommé en _CompteBancaire__solde pour éviter les collisions en héritagePour contrôler l'accès, on utilise @property plutôt que des getters/setters Java :
class Temperature:
def __init__(self, celsius: float):
self.celsius = celsius # passe par le setter
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, valeur: float) -> None:
if valeur < -273.15:
raise ValueError("Sous le zéro absolu")
self._celsius = valeur
@property
def fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
t = Temperature(20)
t.fahrenheit # 68.0 — calculé, sans parenthèses
t.celsius = -300 # ValueErrorfrom datetime import date
class Personne:
def __init__(self, nom: str, naissance: date):
self.nom = nom
self.naissance = naissance
@classmethod
def depuis_chaine(cls, s: str) -> "Personne":
nom, iso = s.split(";")
return cls(nom, date.fromisoformat(iso))
@staticmethod
def est_majeur(age: int) -> bool:
return age >= 18
p = Personne.depuis_chaine("Ada;1815-12-10")@classmethod reçoit la classe (cls) : c'est le pattern des constructeurs alternatifs. @staticmethod ne reçoit rien, c'est une fonction rangée dans la classe.
class Animal:
def __init__(self, nom: str):
self.nom = nom
def parler(self) -> str:
raise NotImplementedError
class Chien(Animal):
def parler(self) -> str:
return f"{self.nom} : Wouf"
class ChienDeGarde(Chien):
def __init__(self, nom: str, zone: str):
super().__init__(nom)
self.zone = zone
for a in [Chien("Rex"), ChienDeGarde("Max", "entrepôt")]:
print(a.parler())super() suit le MRO (Method Resolution Order), visible avec ChienDeGarde.__mro__. Python gère l'héritage multiple, mais je le réserve aux petites classes « mixin » qui ajoutent un comportement isolé.
Un Moteur n'est pas une Voiture. Une voiture a un moteur.
class Moteur:
def demarrer(self) -> str:
return "Vroum"
class Voiture:
def __init__(self):
self.moteur = Moteur()
def demarrer(self) -> str:
return self.moteur.demarrer()L'héritage crée un couplage fort. Dès que la relation « est un » est discutable, composez.
Les méthodes __nom__ permettent à vos objets de s'intégrer au langage : opérateurs, len(), for, with, comparaison.
from functools import total_ordering
@total_ordering
class Vecteur:
def __init__(self, x: float, y: float):
self.x, self.y = x, y
def __add__(self, autre: "Vecteur") -> "Vecteur":
return Vecteur(self.x + autre.x, self.y + autre.y)
def __mul__(self, k: float) -> "Vecteur":
return Vecteur(self.x * k, self.y * k)
def __abs__(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5
def __eq__(self, autre: object) -> bool:
return isinstance(autre, Vecteur) and (self.x, self.y) == (autre.x, autre.y)
def __lt__(self, autre: "Vecteur") -> bool:
return abs(self) < abs(autre)
def __repr__(self) -> str:
return f"Vecteur({self.x}, {self.y})"
Vecteur(1, 2) + Vecteur(3, 4) # Vecteur(4, 6)
abs(Vecteur(3, 4)) # 5.0| Méthode | Déclenchée par |
|---|---|
__init__ | Classe() |
__repr__ / __str__ | repr() / print() |
__len__ | len(obj) |
__getitem__ | obj[cle] |
__iter__ | for x in obj |
__contains__ | x in obj |
__call__ | obj() |
__enter__ / __exit__ | with obj: |
__hash__ | clé de dict, élément de set |
Définir __eq__ sans __hash__ rend l'objet non hashable. C'est voulu : deux objets égaux doivent avoir le même hash.
Pour les classes qui transportent surtout des données, @dataclass génère __init__, __repr__ et __eq__.
from dataclasses import dataclass, field
@dataclass
class Produit:
nom: str
prix: float
tags: list[str] = field(default_factory=list)
@dataclass(frozen=True, slots=True)
class Coordonnee:
lat: float
lon: float
p = Produit("Clavier", 79.9)
p # Produit(nom='Clavier', prix=79.9, tags=[])
Coordonnee(48.8, 2.3) == Coordonnee(48.8, 2.3) # Truefrozen=True rend l'instance immuable et hashable. slots=True supprime le __dict__ par instance : moins de mémoire, accès plus rapide.
abc.ABC force les sous-classes à implémenter certaines méthodes :
from abc import ABC, abstractmethod
from pathlib import Path
class Stockage(ABC):
@abstractmethod
def sauver(self, cle: str, data: bytes) -> None: ...
class StockageDisque(Stockage):
def sauver(self, cle, data):
Path(cle).write_bytes(data)
Stockage() # TypeError : classe abstraiteProtocol exprime le duck typing de manière vérifiable, sans héritage :
from typing import Protocol
class Sauvegardable(Protocol):
def sauver(self, cle: str, data: bytes) -> None: ...
def archiver(s: Sauvegardable) -> None:
s.sauver("backup", b"...")Toute classe qui possède une méthode sauver compatible est acceptée par mypy. Je préfère Protocol à ABC dans la plupart des cas : moins de couplage.
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Article:
nom: str
prix: float
@dataclass
class Panier:
lignes: dict[Article, int] = field(default_factory=dict)
def ajouter(self, article: Article, qte: int = 1) -> None:
self.lignes[article] = self.lignes.get(article, 0) + qte
def __len__(self) -> int:
return sum(self.lignes.values())
@property
def total(self) -> float:
return round(sum(a.prix * q for a, q in self.lignes.items()), 2)
panier = Panier()
panier.ajouter(Article("Livre", 12.5), 2)
panier.ajouter(Article("Stylo", 1.2))
len(panier), panier.total # (3, 26.2)Python pousse vers une POO légère : des dataclasses pour les données, des protocols pour les contrats, de la composition pour l'assemblage. Les hiérarchies profondes à la Java sont presque toujours un signal d'alarme.