pichegru.net

Physique-Chimie & NSI

Cours complets et originaux de Physique-Chimie & NSI

1-03. Programmation objets

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.

La programmation orientée objet a commencé à être utilisée au début des années 1970.

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 :

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

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