Physique-Chimie & NSI

Cours complets et originaux de Physique-Chimie & NSI

1-03. Programmation objet

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.

La POO a pour but de structurer le code des applications complexes en encapsulant données et fonctions dans des classes.

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 :

  1. On n’aura pas la garantie qu’une variable utilisateur possède bien un id, un pseudo et un crédit.
  2. On ne pourra pas attribuer de valeur par défaut à certaines propriétés
  3. Même si la variable possède ces données, on n’aura pas la garantie qu’elles soient cohérentes.
  4. 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.
  5. Le code sera moins lisible et donc moins maintenable.

Voici un exemple sans utilisation de classe :

		
			# 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

			#––– code à exécuter –––––––––
			user1 = { "id" : 24, "pseudo": "toto", "credit" : 2 }
			user1["pseudo"] = "titi"
			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
			CST = "Hello" # propriété commune à tous les objets de cette classe
			def __init__(self, parametre): # méthode lancée à l’instanciation
				self.nom = parametre
	

Quelques remarques

Par convention, on nomme 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.

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.Nom) # affiche "Coucou" – l’attribut nom de l’objet a
			a.nom = "Ciao" # on modifie l’attribut foo de l’objet a
		
	

La classe Table

1. Définissez une classe Table avec trois propriétés : largeur, longueur et hauteur (toutes les dimensions sont données en millimètres). La hauteur par défaut vaut 780 mm.

2. Instanciez un objet de la classe Table.

3. Afficher sa hauteur, puis changez-la.

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 de print() ou qu’on souhaite le convertir en str.
  • __eq__ : permet de définir à quelle(s) condition(s) l’assertion a == b renvoie True quand a et b sont des objets de la même classe.

		class MyClass:

			def __init__(self, parametre):
				self.nom = parametre

			def __str__ (self) : # exécutée via print() ou str()
				return f"nom: {self.nom}"
			
			def __eq__(self, other): # exécutée lors d’un test a == b
				return self.nom == other.nom
			
		# ––––
		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.

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.

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.).