🎯 Objectif : À la fin de ce chapitre, tu maîtriseras la POO en Python — classes, héritage, méthodes spéciales — et l’organisation du code en modules. ⏱️ Durée estimée : 50 minutes | Niveau : Intermédiaire
Pourquoi la POO en DevOps
Tous les outils Python que tu utilises au quotidien — Boto3, Docker SDK, Requests, Paramiko — sont construits avec des classes. Quand tu écris client = boto3.client('s3'), tu instancies un objet. Quand tu fais response.json(), tu appelles une méthode.
Comprendre la POO, c’est comprendre comment ces outils fonctionnent, pouvoir les étendre, et écrire du code maintenable quand tes scripts dépassent 200 lignes.
💡 La POO n’est pas une obligation — un script de 50 lignes n’a pas besoin de classes. Mais dès que tu gères des entités (serveurs, conteneurs, déploiements) avec des comportements, les classes deviennent naturelles.
Créer une classe
Une classe est un modèle qui définit des attributs (données) et des méthodes (comportements).
class Server:
"""Représente un serveur dans l'infrastructure."""
def __init__(self, hostname, ip, role="web"):
self.hostname = hostname
self.ip = ip
self.role = role
self.status = "stopped"
self.services = []
def start(self):
self.status = "running"
print(f"✅ {self.hostname} démarré")
def stop(self):
self.status = "stopped"
print(f"🔴 {self.hostname} arrêté")
def add_service(self, name):
self.services.append(name)
def __str__(self):
return f"Server({self.hostname}, {self.ip}, {self.status})"
Instancier et utiliser
web = Server("web-01", "10.0.1.10")
db = Server("db-01", "10.0.1.20", role="database")
web.start()
web.add_service("nginx")
web.add_service("certbot")
print(web) # Server(web-01, 10.0.1.10, running)
print(web.services) # ['nginx', 'certbot']
🔥 __init__ est le constructeur — il s’exécute automatiquement à la création de l’objet. self réfère toujours à l’instance courante.
Attributs de classe vs d’instance
class Server:
# Attribut de classe : partagé par toutes les instances
total_count = 0
def __init__(self, hostname, ip):
self.hostname = hostname # Attribut d'instance : propre à chaque objet
self.ip = ip
Server.total_count += 1
@classmethod
def get_count(cls):
"""Méthode de classe : accède aux attributs de classe."""
return cls.total_count
@staticmethod
def validate_ip(ip):
"""Méthode statique : pas besoin de self ni cls."""
parts = ip.split(".")
return len(parts) == 4 and all(0 <= int(p) <= 255 for p in parts)
# Usage
s1 = Server("web-01", "10.0.1.1")
s2 = Server("web-02", "10.0.1.2")
print(Server.get_count()) # 2
print(Server.validate_ip("10.0.1.1")) # True
💡 @classmethod reçoit la classe (cls), pas l’instance. Utile pour les factory methods. @staticmethod ne reçoit ni l’un ni l’autre — c’est juste une fonction rangée dans la classe.
Héritage : spécialiser des classes
L’héritage crée des classes enfants qui héritent des attributs et méthodes du parent, et peuvent les étendre ou les modifier.
class Server:
def __init__(self, hostname, ip, role="generic"):
self.hostname = hostname
self.ip = ip
self.role = role
self.status = "stopped"
def start(self):
self.status = "running"
return f"{self.hostname} démarré"
def info(self):
return f"{self.hostname} ({self.ip}) - {self.role} [{self.status}]"
class WebServer(Server):
def __init__(self, hostname, ip, port=80, ssl=False):
super().__init__(hostname, ip, role="web")
self.port = port
self.ssl = ssl
def start(self):
result = super().start() # Appelle la méthode du parent
proto = "https" if self.ssl else "http"
return f"{result} — {proto}://0.0.0.0:{self.port}"
class DatabaseServer(Server):
def __init__(self, hostname, ip, engine="postgresql"):
super().__init__(hostname, ip, role="database")
self.engine = engine
def backup(self, path="/var/backups"):
return f"Backup {self.engine} → {path}"
# Usage
web = WebServer("web-01", "10.0.1.10", port=443, ssl=True)
db = DatabaseServer("db-01", "10.0.1.20")
print(web.start()) # web-01 démarré — https://0.0.0.0:443
print(db.backup()) # Backup postgresql → /var/backups
print(web.info()) # web-01 (10.0.1.10) - web [running]
# isinstance vérifie la hiérarchie
isinstance(web, Server) # True
isinstance(web, WebServer) # True
isinstance(db, WebServer) # False
⚠️ super().__init__() est obligatoire dans le constructeur enfant si tu veux hériter des attributs du parent. Oublie-le, et self.hostname n’existera pas.
Méthodes spéciales (dunder methods)
Les méthodes __xxx__ permettent à tes objets de se comporter comme des types natifs Python.
class Cluster:
def __init__(self, name):
self.name = name
self._nodes = []
def add(self, node):
self._nodes.append(node)
def __len__(self): # len(cluster)
return len(self._nodes)
def __contains__(self, node): # "web-01" in cluster
return node in self._nodes
def __iter__(self): # for node in cluster
return iter(self._nodes)
def __getitem__(self, idx): # cluster[0]
return self._nodes[idx]
def __str__(self): # print(cluster)
return f"Cluster({self.name}, {len(self)} nodes)"
cluster = Cluster("prod")
cluster.add("web-01")
cluster.add("web-02")
cluster.add("db-01")
print(len(cluster)) # 3
print("web-01" in cluster) # True
print(cluster[0]) # web-01
for node in cluster:
print(f" - {node}")
Properties : contrôler l’accès aux attributs
class Container:
def __init__(self, name, cpu_limit=100):
self.name = name
self._cpu_limit = cpu_limit
@property
def cpu_limit(self):
return self._cpu_limit
@cpu_limit.setter
def cpu_limit(self, value):
if not 0 < value <= 100:
raise ValueError("cpu_limit doit être entre 1 et 100")
self._cpu_limit = value
c = Container("worker", 50)
c.cpu_limit = 80 # OK
# c.cpu_limit = 200 # ValueError
🎯 Les properties donnent une interface propre (attribut) avec une logique de validation (méthode). C’est le meilleur des deux mondes.
Modules et packages
Importer des modules
Un module est simplement un fichier .py. Un package est un dossier contenant un __init__.py.
# Imports de la bibliothèque standard
import os
import json
from pathlib import Path
from datetime import datetime, timedelta
# Import sélectif
from os.path import exists, join
# Import avec alias
import subprocess as sp
# Usage
cwd = os.getcwd()
config = json.loads('{"port": 8080}')
now = datetime.now()
Organiser son code en package
infra/
├── __init__.py # Marque le dossier comme package
├── servers.py # Classes Server, WebServer, etc.
├── monitoring.py # Fonctions de monitoring
└── utils.py # Utilitaires (validation, formatage)
main.py
# infra/__init__.py
from .servers import Server, WebServer, DatabaseServer
from .monitoring import check_health
# main.py
from infra import Server, WebServer, check_health
web = WebServer("web-01", "10.0.1.10", port=443)
💡 Le __init__.py définit l’API publique du package. Les utilisateurs importent depuis infra sans connaître la structure interne.
Modules utiles de la bibliothèque standard
# os / pathlib : système de fichiers
from pathlib import Path
config_dir = Path("/etc/myapp")
config_dir.mkdir(parents=True, exist_ok=True)
files = list(config_dir.glob("*.yaml"))
# subprocess : exécuter des commandes
import subprocess
result = subprocess.run(
["docker", "ps", "--format", "{{.Names}}"],
capture_output=True, text=True
)
containers = result.stdout.strip().split("\n")
# json : sérialisation
import json
data = {"servers": ["web-01", "web-02"]}
json_str = json.dumps(data, indent=2)
parsed = json.loads(json_str)
# Lire/écrire des fichiers JSON
with open("config.json") as f:
config = json.load(f)
with open("output.json", "w") as f:
json.dump(data, f, indent=2)
Cas entreprise : gestionnaire d’infrastructure
Contexte : tu crées un outil CLI pour gérer l’inventaire de tes serveurs. Les classes structurent le code, les modules le rendent réutilisable.
import json
from datetime import datetime
class Server:
def __init__(self, hostname, ip, role, env="production"):
self.hostname = hostname
self.ip = ip
self.role = role
self.env = env
self.created_at = datetime.now().isoformat()
def to_dict(self):
return {
"hostname": self.hostname,
"ip": self.ip,
"role": self.role,
"env": self.env,
"created_at": self.created_at,
}
class Inventory:
def __init__(self, filepath="inventory.json"):
self.filepath = filepath
self.servers = []
self._load()
def _load(self):
try:
with open(self.filepath) as f:
data = json.load(f)
self.servers = data.get("servers", [])
except FileNotFoundError:
self.servers = []
def save(self):
with open(self.filepath, "w") as f:
json.dump({"servers": self.servers}, f, indent=2)
def add(self, server):
self.servers.append(server.to_dict())
self.save()
def find_by_role(self, role):
return [s for s in self.servers if s["role"] == role]
def summary(self):
roles = {}
for s in self.servers:
roles[s["role"]] = roles.get(s["role"], 0) + 1
return roles
# Usage
inv = Inventory()
inv.add(Server("web-01", "10.0.1.10", "web"))
inv.add(Server("db-01", "10.0.2.10", "database"))
inv.add(Server("mon-01", "10.0.3.10", "monitoring"))
print(f"Web servers: {inv.find_by_role('web')}")
print(f"Résumé: {inv.summary()}")
Ce pattern — une classe métier (Server) + une classe gestionnaire (Inventory) avec persistance JSON — se retrouve partout en DevOps.
Les pièges classiques
⚠️ Oublier self — Toutes les méthodes d’instance prennent self en premier argument. L’oublier cause TypeError: method takes 0 positional arguments.
⚠️ Attributs de classe mutables — class Server: services = [] partage la MÊME liste entre toutes les instances. Initialise les listes dans __init__.
⚠️ Import circulaire — Si a.py importe b.py et b.py importe a.py, Python crashe. Restructure le code ou utilise des imports locaux (dans la fonction).
⚠️ Héritage profond — Plus de 2-3 niveaux d’héritage rend le code difficile à suivre. Préfère la composition : un objet qui contient d’autres objets plutôt qu’un objet qui hérite de tout.
⚠️ Ne pas utiliser super() — Appeler directement Server.__init__(self, ...) au lieu de super().__init__(...) casse l’héritage multiple. Utilise toujours super().
Résumé
La POO et les modules sont les outils pour passer du script jetable au code maintenable :
- Classes →
__init__,self, méthodes,__str__/__repr__ - Héritage →
super(), surcharge de méthodes,isinstance() - Méthodes spéciales →
__len__,__contains__,__iter__pour un comportement natif - Properties →
@property+@setterpour la validation d’attributs - Modules →
import, packages avec__init__.py, bibliothèque standard
🎯 En DevOps, tu n’écriras pas des hiérarchies de classes complexes. Mais une classe Server, un Inventory avec persistance JSON, un Client API — ces patterns reviennent constamment. Maîtrise-les une fois, réutilise-les partout.
➡️ Félicitations ! Tu as terminé la série Python DevOps. Tu as les bases pour écrire des scripts d’automatisation, des outils CLI et des intégrations API en Python.
Contenu réservé aux abonnés
Ce chapitre fait partie de la formation complète. Abonne-toi pour débloquer tous les contenus.
Débloquer pour 29 CHF/moisLe chapitre 1 de chaque formation est gratuit.
Série pas encore débloquée
Termine la série prérequise d'abord pour accéder à ce contenu.
Aller à la série prérequiseSérie : Python DevOps
3 / 8Sur cette page
Articles liés
Structures de contrôle et fonctions
Conditions, boucles for/while, fonctions, et compréhensions de listes en Python. Tout pour écrire tes premiers scripts DevOps.
Python : les bases du langage
Premiers pas en Python pour le DevOps : variables, types de données, opérateurs, strings, listes, dictionnaires, tuples et sets.
Gestion d'erreurs et fichiers
Try/except, manipulation de fichiers (JSON, YAML, CSV), environnements virtuels avec venv/pip, et pattern d'un script DevOps propre.