Exercices

Durée de la session2h30

Présentation & objectifs

Important

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) : ajoute text (une chaîne) au contenu actuel du tableau comme une nouvelle ligne
    • read() : retourne le contenu actuel du tableau sous forme de chaîne
    • show() : affiche le contenu actuel du tableau dans la console
    • erase() : 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, vous allez créer une classe pour représenter une version simple d’un compte bancaire (BankAccount). 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) (ou void deposit(float amount) en Java) : effectue des dépôts sur le compte. Un dépôt ne peut être possible que si amount est un nombre positif. Si ce n’est pas le cas, une exception (ValueError ou IllegalArgumentException en Java) doit être levée
    • withdraw(amount: float) (ou void withdraw(float amount) en Java) : effectue des retraits. Un retrait ne peut être possible que si amount 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 ou IllegalArgumentException en Java) doit être levée
    • get_account_number() -> int (ou int getAccountNumber() en Java) : retourne le numéro de compte
    • get_transaction_history() -> list[float] (ou ArrayList<Float> getTransactionHistory() en Java) : retourne l’ensemble des transactions effectuées sur le compte
    • get_balance()->float (ou float getBalance() en Java) : 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 attribut owner comme une Person qui détient le compte. Le titulaire du compte doit être spécifié lors de la création du compte (sinon une exception (ValueError ou IllegalArgumentException en Java) est levée). De plus, une méthode get_owner() (ou getOwner() en Java) doit être fournie par la classe.

  • créer une classe Person qui caractérise une personne avec son firstname, lastname, age et tous les comptes bancaires qu’elle détient (attribut accounts). La valeur de l’attribut lastname doit toujours être en majuscules. Enfin, il sera possible d’ajouter ou de supprimer un compte.

  1. BankAccount class modifications.
  • The constructor of the class must be in the form __init__(self, account_number: int, owner: Person). It initializes the _owner attribute of the class with the value of the new parameter if it is not None (otherwise, an exception ValueError is raised) and asks the owner to add the new account to his/her list of accounts.

  • The get_owner(self) -> Person method returns the value of the _owner attribute.

  • Add methods __str__(self) -> str and __repr__(self) -> str. Both return a string representation of the bank account and should include the account number, the owner, the balance and the number of transactions done. As mentioned in the course on OOP syntax, both are special methods in Python. To understand the difference between __str__() and __repr__() you can refer to the documentation.

  1. Person class.
  • The class will have the following attributes: _firstname, _lastname, _age and _accounts. The last one will be a list of bank accounts held by the person.

  • The constructor must be in the form __init__(self, name: str, firstname: str, age: int). It initializes all attributes of the class, with the _lastname attribute being in uppercase.

  • The add_account(self, account: BankAccount) -> None method adds account to the list of bank accounts of the owner only if it is not already in the list.

  • The remove_account(self, account: BankAccount) -> None method removes account from the list of bank accounts of the owner if it exists.

  • A method fullname(self) -> str returns a string as the concatenation of the _firstname and _lastname attributes.

  • A method get_accounts(self) -> list[BankAccount] returns the value of the _accounts attribute.

  • A method __str__(self) -> str returns a string representation of the person that includes his/her name, age and the number of accounts he/she owns.

  • A method __eq__(self, other: object) -> bool compares two persons and returns True if they are equal (firstname, lastname and age), False otherwise. To learn more about the special method __eq__() you can refer to the documentation.

  1. Écrivez une fonction __main__. Dans un fichier main.py séparé, elle 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éthode print() 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. Vérifiez que le propriétaire des comptes bancaires créés est la nouvelle personne créée. Qu’est-ce que cela signifie ?
Avertissement

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
  • 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 module typing. Elle est True seulement pendant la vérification statique de type (par exemple, lors de l’utilisation de MyPy). Comme l’import actuel de la classe BankAccount est enveloppé dans une vérification conditionnelle contre TYPE_CHECKING, la classe sera importée pendant la vérification de type et non pendant l’exécution du programme.

Vous pouvez trouver plus d’informations à ce sujet dans la page de documentation.

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 (ou List<Person> assistants en Java) pour stocker ses assistants

  • a 4 nouvelles méthodes

    • hire_assistant(self, assistant: Person) -> None (ou void hireAssistant(Person assistant) en Java) qui ajoute assistant à la liste des assistants s’il n’y est pas déjà
    • fire_assistant(self, assistant: Person) -> None (ou void fireAssistant(Person assistant) en Java) qui supprime assistant de la liste des assistants s’il existe
    • get_assistants(self)-> list[Person] (ou getAssistants() en Java) qui retourne la liste des assistants
    • spend_allocation(self, amount: float) -> dict[str, float] (ou spendAllocation(float amount) en Java) qui distribue équitablement amount parmi les assistants si le amount est positif. Sinon, une exception (ValueError ou IllegalArgumentExceptionen Java) 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__() (@Override public String toString() en Java) 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, et content). Il a un argument (size) comme un int : 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ée
  • close() : 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ée
  • place_in(p: RussianDoll) (void placeIn(RussianDoll p) en Java) : place la poupée actuelle dans la poupée p, 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 actuelle
  • get_out(p: RussianDoll) (void getOut(RussianDoll p) en Java) : sort la poupée actuelle de la poupée p si elle est dans p et si p 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 :

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__ (toString() en Java) 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__ (toString() en Java) à 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__ (ou boolean equals(Object) en Java) 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 au-delà

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 :