Exercices
Durée de la session2h30Présentation & objectifs
L’objectif de cette session est de vous aider à maîtriser des notions importantes en informatique. Un assistant de programmation intelligent tel que GitHub Copilot, que vous avez peut-être déjà installé, sera capable de vous fournir une solution à ces exercices basée uniquement sur un nom de fichier judicieusement choisi.
Dans un souci de formation, nous vous conseillons de désactiver d’abord ces outils.
À la fin de l’activité pratique, nous vous suggérons de retravailler l’exercice avec ces outils activés. Suivre ces deux étapes améliorera vos compétences à la fois fondamentalement et pratiquement.
De plus, nous vous fournissons les solutions aux exercices. Assurez-vous de ne les consulter qu’après avoir une solution aux exercices, à des fins de comparaison ! Même si vous êtes sûr que votre solution est correcte, veuillez y jeter un coup d’œil, car elles fournissent parfois des éléments supplémentaires que vous avez peut-être manqués.
Contenu de l’activité
1 - Un tableau noir
Dans cet exercice, vous allez créer une classe représentant un tableau noir. Votre tableau noir :
- a une surface (un attribut) sous forme de chaîne
- fournit trois méthodes :
write(text)
: ajoutetext
(une chaîne) au contenu actuel du tableau comme une nouvelle ligneread()
: retourne le contenu actuel du tableau sous forme de chaîneshow()
: affiche le contenu actuel du tableau dans la consoleerase()
: supprime le contenu du tableau.
Question. Créez la classe Blackboard
comme décrit ci-dessus.
Question. Créez un tableau noir, et utilisez ses méthodes pour écrire Hello, World!
dessus, lire le message, puis le supprimer et, si le contenu du tableau n’est pas vide, écrire un message d’erreur Error: The blackboard should be empty
.
2 - Un compte bancaire
Dans cet exercice et les suivants, nous avons fait le choix de typer les arguments et les valeurs de retour des méthodes.
Typer une variable signifie indiquer le type de données qu’elle est censée contenir (par exemple, une chaîne de caractères, un entier, un flottant, une liste, etc.).
Cela se fait en suivant le nom de la variable par deux points (:
) et le type de données attendu: p-ex, ma_variable: int
.
Par exemple, la fonction suivante est typée, elle indique prendre une chaîne de caractères et un entier en entrée, et retourne une chaîne de caractères.
def repeat(chaine: str, times: int) -> str:
"""Repeats the input string a given number of times.
"""
result: str = chaine * times
return result
Dans la pratique, typer les arguments et les valeurs de retour des méthodes est une bonne pratique, car cela rend le code plus lisible et aide à détecter les erreurs plus tôt. Typer les variables locales est aussi une bonne pratique, mais moins courante.
Le typage sera vu plus en détail dans la session 2 de Programmation, mais vous pouvez déjà consulter la documentation officielle pour plus d’informations.
Dans cet exercice, vous allez créer une classe pour représenter une version simple d’un compte bancaire (BankAccount
) dans un fichier bankaccount.py
.
Votre compte bancaire :
- est identifié par un numéro (
account_number
) - a le solde actuel du compte (
balance
) et l’historique des transactions effectuées sur le compte (history
) - fournit plusieurs méthodes :
- un constructeur qui initialise le numéro de compte, le solde et l’historique des transactions
deposit(amount: float)
: effectue des dépôts sur le compte. Un dépôt ne peut être possible que siamount
est un nombre positif. Si ce n’est pas le cas, une exception (ValueError
) doit être levéewithdraw(amount: float)
: effectue des retraits. Un retrait ne peut être possible que siamount
est un nombre positif et que le compte a suffisamment d’argent pour couvrir le retrait. Si ce n’est pas le cas, une exception (ValueError
) doit être levéeget_account_number() -> int
: retourne le numéro de compteget_transaction_history() -> list[float]
: retourne l’ensemble des transactions effectuées sur le compteget_balance()->float
: retourne le solde actuel.
Question. Créez la classe BankAccount
comme décrit ci-dessus.
Question. Créez un compte bancaire, effectuez quelques dépôts et retraits avec des montants positifs et négatifs, puis affichez l’historique des transactions et le solde. Vérifiez que les exceptions sont levées comme attendu.
3 - Un compte bancaire et son propriétaire
Supposons que nous voulions savoir pour un compte bancaire qui est son propriétaire et pour chaque personne quels sont ses comptes. Pour cela, vous devez :
-
modifier votre classe
BankAccount
pour ajouter un attributowner
comme unePerson
qui détient le compte. Le titulaire du compte doit être spécifié lors de la création du compte (sinon une exception (ValueError
) est levée). De plus, une méthodeget_owner()
doit être fournie par la classe. -
créer une classe
Person
qui caractérise une personne avec sonfirstname
,lastname
,age
et tous les comptes bancaires qu’elle détient (attributaccounts
). La valeur de l’attributlastname
doit toujours être en majuscules. Enfin, il sera possible d’ajouter ou de supprimer un compte.
- Modifications de la classe
BankAccount
.
- Le constructeur de la classe doit être de la forme
__init__(self, account_number: int, owner: Person)
. Il initialise l’attribut_owner
de la classe avec la valeur du nouveau paramètre si celui-ci n’est pasNone
(sinon, une exceptionValueError
est levée) et demande au propriétaire d’ajouter le nouveau compte à sa liste de comptes. - La méthode
get_owner(self) -> Person
retourne la valeur de l’attribut_owner
. - Ajoutez les méthodes
__str__(self) -> str
et__repr__(self) -> str
. Les deux retournent une représentation sous forme de chaîne du compte bancaire et doivent inclure le numéro de compte, le propriétaire, le solde et le nombre de transactions effectuées. Comme mentionné dans le cours sur la syntaxe OOP, ce sont deux méthodes spéciales en Python. Pour comprendre la différence entre__str__()
et__repr__()
, vous pouvez consulter la documentation ou cet article.
- Classe
Person
.
Créer la classe Person
dans un fichier person.py
séparé.
- La classe aura les attributs suivants :
_firstname
,_lastname
,_age
et_accounts
. Le dernier sera une liste des comptes bancaires détenus par la personne. - Le constructeur doit être de la forme
__init__(self, name: str, firstname: str, age: int)
. Il initialise tous les attributs de la classe, avec l’attribut_lastname
en majuscules. - La méthode
add_account(self, account: BankAccount) -> None
ajouteaccount
à la liste des comptes bancaires du propriétaire uniquement s’il n’est pas déjà dans la liste. - La méthode
remove_account(self, account: BankAccount) -> None
retireaccount
de la liste des comptes bancaires du propriétaire s’il existe. - Une méthode
fullname(self) -> str
retourne une chaîne correspondant à la concaténation des attributs_firstname
et_lastname
. - Une méthode
get_accounts(self) -> list[BankAccount]
retourne la valeur de l’attribut_accounts
. - Une méthode
__str__(self) -> str
retourne une représentation sous forme de chaîne de la personne, incluant son nom, son âge et le nombre de comptes qu’elle possède. - Une méthode
__eq__(self, other: object) -> bool
compare deux personnes et retourneTrue
si elles sont égales (prénom, nom et âge),False
sinon. Pour en savoir plus sur la méthode spéciale__eq__()
, vous pouvez consulter la documentation.
- Écrivez une fonction
__main__
. Dans un fichiermain.py
séparé, cette fonction crée une personne et trois comptes, effectue quelques transactions dans au moins l’un d’entre eux puis le supprime. Enfin, affichez le solde et les transactions effectuées dans tous les comptes bancaires de la personne. Utilisez la méthodeprint()
pour vous assurer que votre code fonctionne. Optionnel Créez une nouvelle personne avec les mêmes prénom, nom et âge que la première. Comparez le propriétaire des comptes bancaires avec cette nouvelle personne. Que remarquez-vous ? Pourquoi ? Affichez les comptes de la personne nouvellement créée, que remarquez-vous ? Pourquoi ?
Pour que votre code s’exécute, vous devrez ajouter les lignes de code suivantes au début du fichier person.py
:
from __future__ import annotations
from typing import *
if TYPE_CHECKING:
from bankaccount import BankAccount
Ces deux lignes ensemble évitent les erreurs liées aux références cycliques. Dans notre cas, la classe Person
référence la classe BankAccount
, et la classe BankAccount
référence la classe Person
. Pour briser ce cycle de référence, les annotations de type sont différées en désactivant leur évaluation au moment de l’exécution, et les imports sont restreints à la phase de vérification de type.
Plus précisément :
- la première ligne importe le module
annotations
du package__future__
, disant à Python de traiter les annotations de type comme des littéraux de chaîne plutôt que de les évaluer immédiatement au moment de l’exécution, - dans la deuxième ligne, la constante
TYPE_CHECKING
est un booléen spécial défini dans le moduletyping
. Elle estTrue
seulement pendant la vérification statique de type (par exemple, lors de l’utilisation de MyPy). Comme l’import actuel de la classeBankAccount
est enveloppé dans une vérification conditionnelle contreTYPE_CHECKING
, la classe sera importée pendant la vérification de type et non pendant l’exécution du programme.
4 - Représentants élus
Dans cet exercice, vous allez considérer le cas particulier des personnes qui sont des représentants élus. Les représentants élus sont des personnes avec un ensemble d’assistants (qui sont aussi des personnes, bien sûr). Un élu peut embaucher ou licencier un assistant. Il/elle peut aussi distribuer un budget à ses assistants : il/elle divise la somme qui lui est allouée équitablement entre ses assistants en ajoutant de l’argent à l’un des comptes bancaires des assistants. Plus spécifiquement, un élu :
-
est une personne avec un nouvel attribut
_assistants
pour stocker ses assistants -
a 4 nouvelles méthodes
hire_assistant(self, assistant: Person) -> None
qui ajouteassistant
à la liste des assistants s’il n’y est pas déjàfire_assistant(self, assistant: Person) -> None
qui supprimeassistant
de la liste des assistants s’il existeget_assistants(self)-> list[Person]
qui retourne la liste des assistantsspend_allocation(self, amount: float) -> dict[str, float]
qui distribue équitablementamount
parmi les assistants si leamount
est positif. Sinon, une exception (ValueError
) est levée. La méthode retourne les assistants qui n’ont pas de compte bancaire et le montant qui devrait leur être donné (par d’autres moyens que le virement bancaire). Si un assistant a plus d’un compte, celui avec le solde le plus faible est utilisé.
Question. Créez la classe ElectedOfficial
comme décrit ci-dessus. Utilisez les classes Person
et BankAccount
de l’exercice précédent. Pour implémenter l’élément clé du dictionnaire à retourner, utilisez le nom complet d’un assistant.
Question. Ajoutez une méthode __str__()
pour décrire votre nouvelle classe. Pour cela, redéfinissez la méthode __str__()
de la classe parente.
Question. Créez un représentant élu et trois personnes : une sans compte bancaire, la deuxième avec un compte bancaire et la dernière avec deux comptes bancaires avec des soldes différents. Le représentant élu embauche le premier assistant et essaie de dépenser une allocation de 1000 euros. Puis il/elle embauche la deuxième personne et essaie de distribuer 2000 euros. Enfin, il/elle embauche la troisième personne et distribue 1500 euros.
5 - Optimisez vos solutions
Ce que vous pouvez faire maintenant, c’est utiliser des outils IA tels que GitHub Copilot ou ChatGPT, soit pour générer la solution, soit pour améliorer la première solution que vous avez trouvée ! Essayez de faire cela pour tous les exercices ci-dessus, pour voir les différences avec vos solutions.
Pour aller plus loin
6 - Poupées russes
Dans cet exercice, vous allez écrire un programme simulant des poupées russes de différentes tailles. Chaque poupée a une taille donnée, peut s’ouvrir ou se fermer, peut contenir une autre poupée et être contenue dans une autre poupée. Écrivez une classe RussianDoll
contenant les méthodes suivantes :
- un constructeur qui initialise les attributs (
size
,opened
,placed_in
, etcontent
). Il a un argument (size
) comme unint
: plus la valeur est grande, plus la poupée russe est grande open()
: ouvre la poupée si elle n’est pas déjà ouverte et si elle n’est pas à l’intérieur d’une autre poupéeclose()
: ferme la poupée si elle n’est pas déjà fermée et si elle n’est pas à l’intérieur d’une autre poupéeplace_in(p: RussianDoll)
: place la poupée actuelle dans la poupéep
, si possible. La poupée actuelle doit être fermée et ne pas être déjà à l’intérieur d’une autre poupée,p
doit être ouverte et ne contenir aucune poupée, et elle doit être plus grande que la poupée actuelleget_out(p: RussianDoll)
: sort la poupée actuelle de la poupéep
si elle est dansp
et sip
est ouverte
Écrivez un programme qui vous permet de créer et manipuler des objets poupée.
7 - Repas
Nous voulons développer un programme de gestion de recettes pour un restaurant. Un programmeur a déjà écrit la classe Ingredient
donnée ci-dessous :
class Ingredient:
"""
Represents an ingredient with a name, quantity, state and unit.
"""
def __init__(self, name: str, quantity: int, state: str, unit: str) -> None:
"""
Initializes a new instance of the Ingredient class.
Parameters:
-----------
name: str
The name of the ingredient.
quantity: int
The quantity of the ingredient.
state: str
The state of the ingredient (raw or cooked).
unit: str
The unit of the ingredient (g, kg, ml, cl, l).
Raises:
-------
ValueError: If the quantity is negative, the unit is not valid or the state is not valid.
"""
if quantity < 0:
raise ValueError("Quantity must be positive")
if unit.lower() not in ("g", "kg", "ml", "cl", "l"):
raise ValueError("Unit must be 'g','kg', 'ml', 'cl', or 'l'")
if state.lower() not in ("raw", "cooked"):
raise ValueError("State must be 'raw' or 'cooked'")
self._name: str = name
self._quantity: int = quantity
self._unit: str = unit.lower()
self._state: str = state.lower()
def __str__(self) -> str:
"""
Returns a string representation of the Ingredient object.
"""
return f"{self._quantity}{self._unit} {self._name} ({self._state})"
if __name__ == "__main__":
butter = Ingredient("butter", 250, "raw", "g")
milk = Ingredient("milk", 1000, "raw", "ml")
print(str(butter))
print(str(milk))
L’état d’un ingrédient peut être cooked
ou raw
et l’unité soit une unité de poids (g
, kg
) soit une unité de volume (l
, ml
, cl
). L’état et l’unité sont stockés en minuscules.
Question 1. Ajoutez un attribut price
à la classe Ingredient
. Le prix doit être donné lors de la création d’un ingrédient. N’oubliez pas de modifier la méthode __str__
pour inclure le prix.
Question 2. Écrivez une classe Meal
qui représente un repas, chaque repas ayant un nom et une liste d’ingrédients. Le nom du repas doit être donné au moment de la création. La liste des ingrédients cependant, peut être vide. Vous devriez aussi pouvoir ajouter et supprimer un ingrédient d’un repas.
Question 3. Ajoutez une méthode __str__
à la classe Meal
qui affiche le nom du repas, suivi de son prix (la somme du prix de chaque ingrédient) et la liste des ingrédients. Par exemple, pour le repas pizza Margarita :
Pizza Margherita - 7.84€
- 260g Pizza Dough (raw, 1.15€),
- 200g Tomato Sauce (cooked, 2.27€),
- 200g Mozzarella (raw, 1.19€),
- 60g Parmigiano Reggiano (raw, 1.83€),
- 30g Basil (raw, 1.4€)
Question 4. Écrivez une méthode main
qui crée un repas appelé pizza_margharita
contenant les ingrédients listés dans la question précédente.
Affichez le repas pour vérifier que la méthode __str__
fonctionne correctement.
Question 5. Nous voulons comparer les repas et donc leurs ingrédients. Ajoutez une méthode __eq__
dans la classe Ingredient
qui retourne vrai si deux ingrédients ont le même nom d’aliment et le même état (pas nécessairement la même quantité). Ajoutez une méthode __eq__
dans la classe Meal
qui retourne vrai si deux repas contiennent les mêmes ingrédients.
Pour aller encore plus loin
En structurant (bien) votre code sous forme de classes, vous disposez d’un code plus facilement compréhensible et extensible. Bien que nous allons revenir un peu plus tard sur les bonnes pratiques de programmation, vous pouvez déjà regarder dans les conventions de nommages les recommandations concernant les classes, les objets, propriétés et méthodes :