1-03. Programmation objets
- Écrire la définition d’une classe.
- Accéder aux attributs et méthodes d’une classe.
Vous avez utilisé en classe de première des structures de données natives : int
, float
, str
, list
, dict
… Nous allons voir dans ce chapitre les bases qui vont nous permettre de créer nos propres structures de données.
Les classes, c’est-à-dire la définition d’un type d’objet, sont les principaux outils de la programmation orientée objet (POO). Ce type de programmation permet de structurer les applications complexes en les organisant de façon modulaire comme des ensembles d’objets représentant un concept, une idée ou une entité (comme une personne, un livre, une page d’un site web…), et interagissant entre eux.
Un objet contient des propriétés (des données qui lui sont propres) ainsi que des méthodes (des fonctions qui lui sont propres).
Regardez l’exemple suivant :
a = [0, 5, 7] # on crée une variable de type "list"
a.append(8) # on utilise une méthode propre aux objets de type "list"
On a utilisé la méthode append()
propre aux listes afin d’ajouter un élément en dernière position.
En fait, tous les modules et bibliothèques sont construits en utilisant des objets. IL s’agit du paradigme de programmation le plus courant (et de très loin) : vous ne ferez concrètement rien sans lui.
Principes de base
Vocabulaire
Classe | Structure de données d’un objet appartenant à cette classe. On y définit les attributs et les méthodes. C’est le modèle suivant lequel on crée les objets. |
---|---|
Objet | Instance d’une classe. C’est une entité créée en utilisant une classe, qui encapsule ses propres données et des méthodes de la classe. |
Attribut | Encore appelé propriété, c’est une donnée propre à l’objet (ou à la classe), définie dans la classe. |
Méthode | Fonction interne de l’objet |
Exemple concret
Imaginons que vous travaillez sur le développement d’une application qui doit gérer ses utilisateurs. Afin que toute la logique propre à la gestion des utilisateurs soit regroupée au même endroit, vous décidez de créer une classe User
.
Dans cette classe, vous commencez par définir trois attributs : id
, pseudo
, et credit
.
Puis vous définissez une méthode modifyCredit()
qui accepte un entier en paramètre et qui met à jour le crédit de l’utisateur.
Ben on pourrait faire ça avec un dictionnaire
et une function
toute simple, non ? 😑
Bien sûr ! Sauf que :
- On n’aura pas la garantie qu’une variable utilisateur possède bien un id, un pseudo et un crédit.
- On ne pourra pas attribuer de valeur par défaut à certaines propriétés
- Même si la variable possède ces données, on n’aura pas la garantie qu’elles soient cohérentes.
- Il faudra définir la fonction
modifyUserCredit()
si on veut s’assurer que le crédit ne soit jamais incohérent. Elle ne sera pas regroupée avec la notion d’utilisateur et elle sera dans l’espace globale de mémoire. - Le code sera moins lisible et donc moins maintenable.
Voici un exemple sans utilisation de classe :
user1 = { "id" : 24, "pseudo": "toto", "credit" : 2 }
# on veut changer le pseudo de l’utilisateur
user1["pseudo"] = "titi"
# fonction qui va faire quelques contrôles lors d’une modification de crédits
def modifyUserCredit(user, credit) :
if (type(credit) != int) :
print("Erreur de format du crédit")
elif (user["credit"] + credit < 0):
print("variation de crédit impossible")
else:
user["credit"] += credit
return user
user1 = modifyUserCredit(user1, -10)
La même chose, mais en utilisant une classe (on reviendra plus loin sur la syntaxe des classes).
class User:
def __init__(self, id, pseudo = "new user", credit = 0):
self.id = int(id) # on est sûr que l’id existe et est un int
self.pseudo = str(pseudo)
self.credit = int(credit)
def modifyCredit(self, credit) :
if (type(credit) != int) :
print("Erreur de format du crédit")
elif (self.credit + credit < 0):
print("variation de crédit impossible")
else:
self.credit += credit
#––– code à exécuter –––––––––
user1 = User(24, "toto", 2) # on crée un user
user1.pseudo = "titi" # on modifie son pseudo
user1.modifyCredit(-1) # on modifie son crédit
Implémentation en Python
- Écrire la définition d’une classe.
- Accéder aux attributs et méthodes d’une classe.
Définir une classe
class MyClass: # annonce la définition de la classe
BAR = "Hello" # propriété commune à les objets de cette classe
def __init__(self, foo): # méthode lancée à l’instanciation
self.foo = foo
Quelques remarques
Par convention, on a l’habitude de nommer une classe en mettant la première lettre en majuscule. Ça permet de les différentier des fonctions de manière visible lorsqu’on écrit ou qu’on lit du code.
Il est possible de définir une propriété (= un attribut) commune à toutes les instances de cette classe. Ça peut parfois avoir son utilité.
La fonction __init()__
est exécuté au moment où on crée une instance de cette classe, c’est-à-dire un nouvel objet appartenant à cette classe. Elle n’est jamais appelée explicitement (d’où le fait qu’elle commence et se termine par deux underscores).
Garder le self contrôle
C’est quoi ce self
dans le code ? 😐
C’est une notion pas immédiatement évidente mais extrêmement important quand on fait de la POO. Le self
renvoie à l’instance de la classe en cours.
Heuuu… OK. Et ça veut dire quoi, concrètement ? 😣
Imaginons qu’avec la classe définie ici, on exécute la ligne suivante :
a = MyClass("Coucou") # l’objet a est une instance de MyClass
À ce moment, la fonction __init()__
va se lancer. Elle prend en argument la variable a
elle-même (via le self
), et le "Coucou"
, qui est la valeur de la variable foo
. Le propriété foo
de l’objet qui est en train d’être instancié se verra donc attribuer la valeur "Coucou"
.
Accéder ou modifier un attribut
Rien de plus simple 😊
a = MyClass("Coucou") # l’objet a est une instance de MyClass
print(a.foo) # affiche "Coucou" – on accède à l’attribut foo de l’objet a
a.foo = "Ciao" # on modifie l’attribut foo de l’objet a
Les méthodes
Les méthodes « magiques »
Toutes les classes possèdent des méthodes qui sont automatiquement appelées dans certaines conditions. __init()__
en est un exemple, mais il y en a d’autres. En voici deux qui peuvent vous être utiles :
__str__
: permet de définir ce qui est affiché lorsqu’on passe l’objet comme argument deprint()
ou qu’on souhaite le convertir enstr
.__eq__
: permet de définir à quelle(s) condition(s) l’assertiona == b
renvoieTrue
quanda
etb
sont des objets de la même classe.
class MyClass:
def __init__(self, foo):
self.foo = foo
def __str__ (self) : # exécutée via print() ou str()
return f"foo: {self.foo}"
def __eq__(self, other): # exécutée lors d’un test a == b
return self.foo == other.foo
# ––––
a = MyClass("Coucou")
print(a) # affiche "foo: Coucou"
b = MyClass("Coucou")
print( a == b) # affiche True
c = MyClass("Salut")
print( a == c) # affiche False
Les autres méthodes
Définir une méthode pour une classe se fait exactement comme une fonction classique. Il suffit de la définir à l’intérieur de la classe. 🖐️ N’oubliez pas que le premier argument de la méthode est toujours le mot-clé self
qui fait référence à l’instanciation courante de la classe (l’objet via lequel est utilisé la méthode).
Figures géométriques
Créer la classe Circle
avec les attributs et méthodes suivants :
- attribut
radius
qui doit être un nombre strictement positif (un contrôle doit être fait au moment de son instanciation) - méthode
getSurface()
qui renvoie la valeur de la surface du cercle - méthode
getPerimeter()
qui renvoie le périmètre du cercle - deux cercles sont considérés comme égaux s’ils ont le même rayon au millième près
Correction
class Circle:
def __init__(self, radius):
if isinstance(radius,(int, float)) and radius > 0:
self.radius = radius
PI = 3.1415927
def getSurface(self):
return self.PI*self.radius**2
def getPerimeter(self):
return 2*self.PI*self.radius
def __eq__(self, other):
return round(self.radius, 3) == round(other.radius, 3)
Mini projet : Le Black Jack
Le Black Jack est un jeu de cartes de casino. Il réunit plusieurs joueurs ainsi que le croupier. Le croupier distribue les cartes aux joueurs et pour la banque. Au départ, chaque joueur reçoit deux cartes. Il peut ensuite en demander d’autres. L’objectif est de faire plus que la banque, sans toutefois dépasser 21.
La banque est un joueur « automatique », c’est-à-dire qu’elle demande une carte tant que son score est strictement inférieur à 17.
Les cartes ont les valeurs en points suivantes :
- Du 2 au 10 : valeur faciale
- Figures (valet, dame, roi) : 10 pts
- As : 1 pt ou 11 pts, au choix du joueur.
Si la banque a un as, celui-ci compte pour 11 pts si cela permet à la banque d’avoir 17 points ou plus (sans excéder les 21 pts).
Le but de ce mini-projet est de créer un jeu de Black Jack simplifié, jouable de 1 à 3 joueurs.
Le jeu de carte
On va commencer par créer une classe CardGame
qui va simuler un ensemble de classique de 52 cartes (13 cartes dans 4 couleurs).
Compléter la classe ci-dessous là où se trouvent des points de suspension :
import random
class CardGame :
def __init__(self):
""" À l’instanciation, on créé les 52 cartes du jeu et on mélange le jeu """
self.cards = [] # le paquet de carte
for color in ["♥", "♦", "♠", "♣"]:
for value in [1,2,3,4,5,6,7,8,9,10,"V","D","R"]:
# ... ajouter la carte au paquet - tuple(valeur, couleur)
random.shuffle(self.cards) # redistribue aléatoirement les cartes
def drawCard(self):
""" Renvoie la première carte et la supprime du paquet """
return self.cards.pop()
Nous allons faire de ce script un module que nous allons appeler dans notre script principal. Vous allez commencer à apprendre le développement en module ! 😎
Dans le dossier contenant votre script principal (appelons-le main.py), créez un fichier cardgame.py. Collez le code définissant la classe CardGame
(y compris l’import du module random !). Supprimer la définition de la classe dans le script principal. Puis importer la classe CardGame
dans votre script principal, avec l’instruction ci-dessous :
from cardgame import CardGame
# vérifiez que tout marche bien
game = CardGame() # on créer un jeu
print(game.drawCard()) # on tire une carte
print(game.cards) # on affiche les cartes restantes
Et voilà. Vous pouvez maintenant vous concentrer sur votre script principal. Tout ce qui concerne le jeu de carte est encapsulé dans une classe, qui se trouve dans un fichier à part. Tout est beaucoup plus clair, découpé et modifiable facilement.
On a un jeu de carte. Il nous faut maintenant des joueurs ! 😊
Les joueurs
On va créer une classe Player
, avec la propriété cards
, une liste des cartes dont il dispose – initialement vide, et une méthode getScore()
.
Cette méthode n’est pas si simple à mettre en place, puisque si le joueur a un (ou plusieurs) as, ils peuvent valoir 1 ou 11 pts au choix du joueur. Il y a donc plusieurs scores possibles avec la même main. Comme toujours, la meilleure stratégie en programmation est de… procrastiner ! 😁 On va attendre d’y voir plus clair et d’avoir une structure plus complète avant de revenir sur cette méthode. Pour l’instant, on prévoit cette méthode et elle doit toujours renvoyer zéro.
Concevez cette classe dans le fichier player.py, puis importez la classe Player
dans votre script principal.
Faites quelques tests pour vous assurer que tout va bien.
from player import Player
player1 = Player()
print(player1.cards) # doit renvoyer une liste vide
print(player1.getScore()) # doit renvoyer zéro
On a maintenant un jeu de carte et des joueurs. Il nous manque le croupier qui distribue les cartes. On va créer une classe Dealer
(oui, c’est comme ça qu’on dit croupier en anglais, je n’y peux rien ! 😏).
Le croupier
Le rôle du croupier est de distribuer des cartes aux joueurs (et à la banque), après éventuellement leur avoir demandé s’ils en voulaient.
Pour l’instant, commençons par créer la classe Dealer
, qui est initiée avec un jeu de carte. Créons la méthode giveCardTo(player: Player)
qui va permettre de donner une carte à un joueur.
from cardgame import CardGame
from player import Player
class Dealer:
def __init__(self):
self.deck = CardGame() # l’attribut "deck" est lui-même un objet de classe CardGame
def giveCardTo(self, player: Player):
# ... (compléter en 2 lignes)
Maintenant, il ne nous manque plus que la partie elle-même, qui va réunir le croupier et les différents joueurs.
La partie de Black Jack
On va également gérer la partie sous forme d’une classe BlackJackGame
. La partie va prendre en argument les différents joueurs (la banque sera traitée automatiquement).
Pour l’instant, fixons-nous comme objectif de créer cette classe qui s’instanciera avec des joueurs. Elle aura également un attribut dealer
de classe Dealer
. La banque est toujours le dernier joueur et sera créé automatiquement au moment de l’instanciation de la partie, sans qu’il y ait besoin de le préciser.
On en profitera, histoire de distinguer les joueurs, pour ajouter l’attribut "Name" à la classe joueur. Celui-ci sera donné au moment de l’instanciation d’un nouveau joueur.
from player import Player
from dealer import Dealer
class BlackJackGame:
def __init__(self, *players: Player):
self.dealer = Dealer()
self.players = []
for p in players:
self.players.append(p)
# ... (une ligne pour ajouter la banque comme joueur, à la fin de players)
Ça y est ! On a tout ce qui faut pour gérer la partie. Il reste maintenant à développer les méthodes adéquates pour pouvoir vraiment jouer. Faisons quelques tests dans notre script principal :
from player import Player
from blackjackgame import BlackJackGame
player1 = Player("Toto")
player2 = Player("Titi")
game = BlackJackGame(player1, player2)
print(game.dealer) # affiche <dealer.Dealer object at 0x00000...>
print(game.players) # affiche [<player.Player object at 0x00...>, ... + 2 autres joueurs]
On remarque que la commande print
appliquée à un objet de la classe Player
n’affiche pas un résultat plaisant… Commençons par changer ça en utilisant la méthode __str__() dans la classe Player
. On veut afficher le nom du joueur, ses cartes et son score. __str__()
est appliquée quand on fait print(player1)
, mais pas print(game.players)
. C’est normal. Pour régler ce problème, ajouter simplement à la classe Player
la méthode __repr__() qui renvoie un appel à la méthode __str__()
. La classe Player devrait maintenant ressembler à ça :
class Player():
def __init__(self, name="Sans nom"):
self.cards = []
self.name = name
def __str__(self):
return f"{self.name} – jeu {self.cards} – score {self.getScore()}"
def __repr__(self):
return self.__str__()
def getScore(self):
return 0
C’est parti !
À partir de là, débrouillez-vous ! 😁. Le fait qu’on travaille en POO nous facilite la tâche. Vous n’avez qu’à imaginer ce qui se passe dans la réalité et créer les méthodes correspondantes dans les classes adéquates. Par exemple, au début, nous réunissons tout ce qui est nécessaire à une partie. Ça se traduit par le code ci-dessous :
from player import Player
from blackjackgame import BlackJackGame
# Par la suite, je ne ferai plus figurer ces imports
player1 = Player("Toto")
player2 = Player("Titi")
game = BlackJackGame(player1, player2)
# À ce stade, on a deux joueurs sans carte, la banque, sans carte également
# et un croupier avec un jeu complet en main
La partie est prête. Par quoi commence-t-elle ? On distribue deux cartes à chaque joueur (en servant la banque en dernier). Pour ça, on a besoin que le croupier donne une carte à chaque joueur, et ce, deux fois de suite. On pourrait créer une méthode qui fait cela, dans la classe BlackJackGame
.
Pour ce qui est du score, dans un premier temps, partez du principe que l’as vaut 1 pt. On verra la subtilité 1 pt / 11 pts à la fin.
Correction
Correction partielle temporaire. Juste avant de vous pencher sur la classe BlackJackGame
, vous devriez avoir ceci :
cardgame.py
import random
class CardGame:
def __init__(self):
# À l’instanciation, on créé les 52 cartes du jeu et on mélange le jeu
self.cards = []
for color in ["♥", "♦", "♠", "♣"]:
for value in [1,2,3,4,5,6,7,8,9,10,"V","D","R"]:
card = (value, color)
self.cards.append(card)
random.shuffle(self.cards)
def drawCard(self):
# Renvoie la première carte et la supprime du paquet
return self.cards.pop()
player.py
class Player:
def __init__(self, name="Sans nom"):
self.cards = [] # liste des cartes du joueur
self.name = name # nom du joueur
self.wantsCard = True
def __str__(self):
# affichage avec la fonction print()
return f"{self.name} – jeu {self.cards} – score {self.getScore()}"
def __repr__(self):
# affichage à l’intérieur d’une liste lorsqu’on fait print(liste)
return self.__str__()
def getScore(self):
# renvoie le score du joueur
score = 0
for card in self.cards:
if card[0] in ["V", "D", "R"]: score += 10
else: score += card[0]
return score
dealer.py
from cardgame import CardGame
from player import Player
class Dealer:
def __init__(self):
self.deck = CardGame() # le paquet de carte associé au dealer
def giveCardTo(self, player: Player):
drawnCard = self.deck.drawCard() # tire une carte du paquet
player.cards.append(drawnCard) # la donne au joueur passé en argument
blackjackgame.py
from player import Player
from dealer import Dealer
class BlackJackGame:
def __init__(self, *players: Player):
self.dealer = Dealer()
self.players = []
# les joueurs passés en argument sont ajoutés à la liste
for p in players: self.players.append(p)
# on ajoute la banque comme dernier joueur
self.players.append(Player("Banque"))
def initiate(self):
for p in self.players:
self.dealer.giveCardTo(p) # on donne une 1e carte à chaque joueur
self.dealer.giveCardTo(p) # on donne une 2nde carte à chaque joueur
def distribute(self):
# Demander à tous les joueurs sauf le dernier (= banque)
for player in self.players[:-1] :
if player.wantsCard:
choice = input(f"{player.name}, voulez-vous une carte supplémentaire ? (oui/non) ")
if choice == "oui": self.dealer.giveCardTo(player)
else: player.wantsCard = False
if player.getScore() >= 21: player.wantsCard = False
# Traiter le cas de la banque
bank = self.players[-1]
if bank.getScore() < 17 :
self.dealer.giveCardTo(bank)
else: bank.wantCards = False
def checkIfSomePlayersStillWantCard(self):
for player in self.players:
if player.wantsCard: return True
return False
main.py
from player import Player
from blackjackgame import BlackJackGame
game = BlackJackGame(Player("Toto"), Player("Titi"))
game.initiate() # distribue 2 cartes à chaque joueur et à la banque
print(game.players)
while game.checkIfSomePlayersStillWantCard() :
game.distribute() # propose une carte à chaque joueur et gère la banque
print(game.players)
Voici une solution possible. On a 4 fichiers module (cardgame.py, player.py, dealer.py, blackjackgame.py) et un fichier main.py qui coordonne tout le monde (et qui ne fait que quelques lignes).
CardGame
import random
class CardGame :
def __init__(self):
""" À l’instanciation, on créé les 52 cartes du jeu et on mélange le jeu """
self.cards = [] # le paquet de carte
for color in ["♥", "♦", "♠", "♣"]:
for value in [1,2,3,4,5,6,7,8,9,10,"V","D","R"]:
self.cards.append((value, color))
random.shuffle(self.cards)
def drawCard(self):
""" Renvoie la première carte et la supprime du paquet """
return self.cards.pop()
Player
class Player():
def __init__(self, name):
self.cards = []
self.name = name
self.wantsCard = True
def __str__(self):
hand = []
for c in self.cards :
hand.append(str(c[0]) + c[1])
return f"{self.name} – {"-".join(hand)} – score {self.getScore()}"
def __repr__(self):
return self.__str__()
def getScore(self):
score = [0]
ace_number = 0
for c in self.cards :
pts = c[0] if isinstance(c[0], int) else 10
score[0] += pts
if c[0] == 1: ace_number += 1
if score[0] >= 21: self.wantsCard = False
for a in range(ace_number): score.append(score[0] + (a+1)*10)
return score
Dealer
from cardgame import CardGame
from player import Player
class Dealer:
def __init__(self):
self.deck = CardGame()
def giveCardTo(self, player: Player):
card = self.deck.drawCard()
player.cards.append(card)
BlackJackGame
from player import Player
from dealer import Dealer
class BlackJackGame:
def __init__(self, *players: Player):
self.dealer = Dealer()
self.players = []
for p in players:
self.players.append(p)
self.players.append(Player("Banque"))
def initiateCards(self):
for p in self.players: self.dealer.giveCardTo(p) # on donne une 1e carte
for p in self.players: self.dealer.giveCardTo(p) # on donne une 2e carte
def proposeCard(self):
end_game = False
while end_game == False :
end_game = True
for p in self.players:
if(p.wantsCard) :
answer = input(f"joueur {p.name}, voulez-vous une carte ? (o/n) ")
if answer.lower() == "o" :
self.dealer.giveCardTo(p)
print(p)
end_game = False # on empêche le jeu de se terminer
else :
p.wantsCard = False
fichier main.py
from player import Player
from blackjackgame import BlackJackGame
player1 = Player("Toto")
player2 = Player("Titi")
game = BlackJackGame(player1, player2)
# On donner deux cartes à tous les joueurs
game.initiateCards()
print(game.players)
# On propose de cartes aux joueurs
game.proposeCard()
print(game.players)
Pour aller plus loin
Ce qui suit est hors programme. Mais il me semble important que vous ayez au moins connaissance de l’existence de ces notions.
Ce qu’on vient de voir sur la POO, c’est vraiment la base de la base… 😅 Ça permet de faire beaucoup de chose, mais en général, on ne s’arrête pas là.
Voici deux notions qui permettent d’aller plus loin en POO.
Héritage
Il est possible de créer une classe en étendant une classe existante et en lui rajoutant des attributs et méthodes.
class User:
def __init__(self, id, name):
self.id = id
self.name = name
self.isConnected = False
self.isBanned = False
def __str__(self):
string = f"{self.getStatus()} {self.name} ({self.id})"
if self.isConnected: string += " is connected"
if self.isBanned: string += " is banned"
return string
def getAccess(self):
if not self.isBanned: self.isConnected = True
def getStatus(self):
return "User"
# Admin hérite de User
class Admin(User):
def getStatus(self):
return "Admin"
def banUser(self, user: User):
user.isBanned = True
user.isConnected = False
user = User(43, "Toto")
admin = Admin(1, "Boss") # Admin s’instancie comme user (hérité)
user.getAccess()
admin.getAccess() # Admin a hérité de la méthode getAccess
user.banUser(admin) # erreur, User ne dispose pas de cette méthode
admin.banUser(user)
print(user)
print(admin)
L'héritage est un mécanisme fondamental de la POO qui permet à une classe (appelée classe enfant) d'acquérir les attributs et méthodes d'une autre classe (appelée classe parent), tout en ayant la possibilité d'ajouter ses propres fonctionnalités ou de redéfinir celles héritées (polymorphisme).
C'est un concept qui favorise la réutilisation du code et permet de créer une hiérarchie organisée de classes partageant des caractéristiques communes.
Polymorphisme
Le polymorphisme, c’est le fait que, dans deux classes différentes, une méthode peut avoir le même nom et pourtant avoir une action différente.
Dans l’exemple des classes User
et Admin
du paragraphe précédent, la méthode getStatus()
est un exemple de polymorphisme : elle renvoie une valeur différente pour chaque classe.
La méthode __str__()
est un exemple de polymorphisme natif en Python. En Python, de nombreuses classes intégrées (built-in) implémentent leur propre version de __str__()
:
- Les chaînes de caractères (
str
) retournent simplement elles-mêmes - Les listes (
list
) affichent leurs éléments entre crochets - Les dictionnaires (
dict
) montrent leurs paires clé-valeur - Vos classes personnalisées peuvent définir leur propre représentation textuelle
Quand vous appelez print(objet)
, Python appelle en coulisses str(objet)
, qui lui-même invoque la méthode __str__()
de l'objet. Selon le type réel de l'objet, une implémentation différente de __str__()
est exécutée.
C'est la définition du polymorphisme : une même interface (ici __str__()
) avec différentes implémentations selon le type d'objet, et le système choisit automatiquement la bonne implémentation à l'exécution.
D'ailleurs, Python utilise abondamment ce principe avec ses "méthodes magiques" (__len__
, __add__
, __eq__
, etc.) qui permettent à différentes classes de réagir différemment aux mêmes opérations (comme +
, ==
, len()
, etc.).