Stockons un compte dans un dictionnaire. Rien ne protège ses données.
user = {"username": "Chris", "role": "analyste",
"failed_attempts": 0}
user["role"] = "n'importe quoi" # aucun contrôle
user["failed_attemps"] = 5 # faute de frappe :
# une nouvelle clé !
| Terme | Définition | Exemple |
|---|---|---|
| Classe | Le modèle : il décrit les données et les actions communes | User |
| Objet (instance) | Un exemplaire concret, créé à partir de la classe | chris |
| Attribut | Une donnée propre à l'objet : son état | chris.role |
| Méthode | Une action que l'objet sait faire : une fonction de la classe | chris.record_failure() |
| Instancier | Créer un objet à partir de sa classe | User("Chris", "analyste") |
class User:
"""Un compte utilisateur."""
def __init__(self, username, role):
self.username = username
self.role = role
self.failed_attempts = 0
self.is_locked = False
class User: définit la classe, nommée en PascalCase__init__ est le constructeur. Il est appelé automatiquement à la création de chaque objetself désigne l'objet courant. self.role = role range la valeur dans cet objetfailed_attempts = 0
from user import User
chris = User("Chris", "analyste")
manon = User("Manon", "auditeur")
print(chris.username) # Chris
print(manon.role) # auditeur
print(type(chris)) # <class 'user.User'>
chris.failed_attempts = 2
print(manon.failed_attempts) # 0
from user import User importe la classe depuis le fichier user.py__init__chris.usernamechris ne modifie pas manon
accounts/
├── user.py # User et Admin
├── server.py # Server
├── network.py # Network
└── main.py # le programme
Les fichiers s'écrivent en minuscules (user.py) et les classes en PascalCase (User), comme le recommande la PEP 8.
main.py utilise les classes sans les définir.
On retrouve vite le code et on modifie un domaine sans toucher aux autres.
class User:
def __init__(self, username, role):
... # le constructeur déjà vu
def record_failure(self):
"""Enregistre un échec et verrouille au 3e."""
self.failed_attempts += 1
if self.failed_attempts >= 3:
self.is_locked = True
from user import User
chris = User("Chris", "analyste")
for _ in range(3):
chris.record_failure()
print(chris.is_locked) # True
self. Elle agit sur l'objet qui l'appellechris.record_failure() revient à écrire User.record_failure(chris)
class User:
MAX_ATTEMPTS = 3 # attribut de classe
def __init__(self, username, role):
self.username = username # d'instance
...
def record_failure(self):
self.failed_attempts += 1
if self.failed_attempts >= User.MAX_ATTEMPTS:
self.is_locked = True
print(User.MAX_ATTEMPTS) # 3
print(chris.MAX_ATTEMPTS) # 3 : lu depuis la classe
self.x) : une valeur par objet__str__Sans __str__
>>> print(chris)
<__main__.User object at 0x10291aa50>
Avec __str__
class User:
...
def __str__(self):
state = "actif"
if self.is_locked:
state = "verrouillé"
return f"{self.username} ({self.role}, {state})"
>>> print(chris)
Chris (analyste, verrouillé)
__str__ est une méthode spéciale. print() et str() l'appellent automatiquement__ (dunder) sont réservées à Python : __init__, __str__…
class Probe:
total = 0
def __init__(self, name):
self.name = name
self.hits = 0
Probe.total += 1
def hit(self):
self.hits += 1
return self.hits
ssh = Probe("ssh")
web = Probe("web")
ssh.hit()
ssh.hit()
web.hit()
print(ssh.hits, web.hits)
print(Probe.total)
print(ssh.hit())
2 1 : chaque sonde compte ses propres passages2 : total est un attribut de classe, incrémenté à chaque création3 : hit() incrémente puis renvoie la nouvelle valeurL'encapsulation consiste à garder la maîtrise des données d'un objet. On les modifie par ses méthodes plutôt que directement.
class User:
def __init__(self, username, password):
self.username = username # public
self._sessions = [] # usage interne
self.__password = password # nom modifié
| Nom | Signification |
|---|---|
username | public |
_sessions | « ne pas toucher » : simple convention |
__password | Python le renomme en _User__password |
_ est un simple signal adressé aux autres développeurs, que Python ne vérifie pas.
Le __ va plus loin. Python renomme l'attribut, ce qui évite de le lire ou de l'écraser par erreur depuis l'extérieur ou depuis une classe fille.
>>> chris = User("Chris", "S3cr3t!")
>>> chris.__password
AttributeError: 'User' object has no attribute
'__password'
>>> chris._User__password
'S3cr3t!'
>>> vars(chris)
{'username': 'Chris', '_sessions': [],
'_User__password': 'S3cr3t!'}
vars() affiche tous les attributs d'un objet, « privés » compris@property : un attribut en lecture seule
class User:
def __init__(self, username):
self.username = username
self._failed_attempts = 0
@property
def failed_attempts(self): # lecture seule
return self._failed_attempts
def record_failure(self):
self._failed_attempts += 1
chris = User("Chris")
chris.record_failure()
print(chris.failed_attempts) # 1
chris.failed_attempts = 5 # AttributeError
@property transforme une méthode en attribut calculé. On l'utilise sans parenthèsesrecord_failure(), modifient _failed_attempts@property : valider une écriture
ROLES = ["analyste", "auditeur", "admin"]
class User:
def __init__(self, username, role):
self.username = username
self.role = role # passe par le setter
@property
def role(self):
return self._role
@role.setter
def role(self, value):
if value not in ROLES:
raise ValueError(f"rôle inconnu : {value}")
self._role = value
chris = User("Chris", "analyste")
chris.role = "auditeur" # accepté
chris.role = "hacker"
# ValueError: rôle inconnu : hacker
__init__
class Admin(User):
def __init__(self, username, scope):
super().__init__(username, "admin")
self.scope = scope # attribut en plus
from user import User, Admin
root = Admin("root", "serveurs Linux")
print(root.username, root.role) # root admin
root.record_failure() # méthode héritée
print(root.failed_attempts) # 1
print(isinstance(root, User)) # True
class Admin(User): signifie que la classe fille Admin hérite de la classe mère Usersuper().__init__(…) appelle le constructeur de la classe mère. Sans lui, username n'existerait pasAdmin est un User ». Ce test vaut pour toute classe mère et sa classe fille.
class User:
...
def describe(self):
return f"Compte {self.username} ({self.role})"
class Admin(User):
...
def describe(self): # redéfinition
return f"Administrateur {self.username}"
print(User("Chris", "analyste").describe())
# Compte Chris (analyste)
print(Admin("root", "serveurs Linux").describe())
# Administrateur root
super()
class Admin(User):
...
def describe(self):
base = super().describe() # version de User
return f"{base} – périmètre : {self.scope}"
root = Admin("root", "serveurs Linux")
print(root.describe())
# Compte root (admin) – périmètre : serveurs Linux
super() donne accès à la version de la méthode de la classe mèreUser.describe() change, Admin suit automatiquement
from user import User, Admin
accounts = [
User("Chris", "analyste"),
Admin("root", "serveurs Linux"),
User("Manon", "auditeur"),
]
for account in accounts:
print(account.describe())
Compte Chris (analyste)
Compte root (admin) – périmètre : serveurs Linux
Compte Manon (auditeur)
isinstance()
admins = 0
for account in accounts:
if isinstance(account, Admin):
admins += 1
print(admins) # 1
print(isinstance(accounts[1], User)) # True
print(type(accounts[1]) == User) # False
isinstance(objet, Classe) est vrai si l'objet est de cette classe ou d'une classe filletype(…) == … ignore l'héritage. Préférez isinstance()
class Alert:
def __init__(self, source):
self.source = source
def level(self):
return "info"
def report(self):
return f"[{self.level()}] {self.source}"
class CriticalAlert(Alert):
def level(self):
return "CRITIQUE"
alerts = [Alert("ssh"), CriticalAlert("firewall")]
for alert in alerts:
print(alert.report())
print(isinstance(alerts[1], Alert))
[info] ssh : méthodes de Alert[CRITIQUE] firewall : report() est héritée, mais self.level() appelle la version redéfinieTrue : une CriticalAlert est une Alert| Relation | Exemple | En Python |
|---|---|---|
| Est un → héritage | Un administrateur est un utilisateur | class Admin(User): |
| A un → composition | Un réseau a des serveurs | self.servers = [] |
class Server:
def __init__(self, hostname, ip, services):
self.hostname = hostname
self.ip = ip
self.services = services # chaînes
class Network:
def __init__(self, name):
self.name = name
self.servers = [] # objets Server
Network n'est pas un Server. Il en contient. En cas de doute, préférez la composition à l'héritage.
class Network:
MAX_SERVERS = 3
def __init__(self, name):
self.name = name
self.servers = [] # objets Server
def add_server(self, server):
if len(self.servers) >= Network.MAX_SERVERS:
raise ValueError("capacité maximale atteinte")
self.servers.append(server)
def find_by_ip(self, ip):
for server in self.servers:
if server.ip == ip:
return server
return None
from network import Network
from server import Server
dmz = Network("DMZ")
dmz.add_server(Server("web01",
"10.0.1.10", ["http", "https"]))
dmz.add_server(Server("mail01",
"10.0.1.20", ["smtp"]))
dmz.find_by_ip("10.0.1.20").hostname
# 'mail01'
L'UML (Unified Modeling Language) est une notation standard pour dessiner un programme avant de le coder. Son diagramme de classes montre les classes et leurs relations.
Trois cases : le nom, les attributs puis les méthodes.
| Symbole | Signification |
|---|---|
+ | public : username |
# | protégé (classe et classes filles) : _nom en Python |
- | privé : en Python, __nom |
| héritage : la pointe creuse vise la classe mère | |
| composition : le losange plein est du côté du conteneur | |
| agrégation : les objets existent sans le conteneur | |
0..3, * | multiplicité : combien d'objets sont liés |
| Notion | Exemple du cours | À retenir |
|---|---|---|
Classe, __init__, self | User | le modèle ; __init__ initialise, self désigne l'objet |
| Méthode | record_failure() | une action que l'objet sait faire |
| Attribut de classe | MAX_ATTEMPTS, MAX_SERVERS | partagé par tous les objets de la classe |
__str__ | print(chris) | rend l'affichage d'un objet lisible |
@property | failed_attempts, role | lecture seule ou écriture validée |
Héritage et super() | Admin(User) | reprendre une classe puis la compléter |
| Polymorphisme | describe() | même appel, comportement propre à chaque classe |
isinstance() | compter les administrateurs | tester le type d'un objet |
| Composition | Network → Server | un objet en contient d'autres (« a un ») |
| Responsabilité unique | user.py, main.py | un fichier par domaine |
| Diagramme UML | vue d'ensemble | cases, visibilité (+ # -), flèches |