Aller au contenu principal
PythonDevOpsFormationPOO

Python avancé : classes et modules

30 min de lecture Python DevOps — Chapitre 3

Programmation orientée objet en Python : classes, héritage, méthodes spéciales, et organisation du code avec les modules et packages.

🎯 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 mutablesclass 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éritagesuper(), surcharge de méthodes, isinstance()
  • Méthodes spéciales__len__, __contains__, __iter__ pour un comportement natif
  • Properties@property + @setter pour la validation d’attributs
  • Modulesimport, 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.

Articles liés