Exercices
Durée de la session2h30Consignes globales
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é
Préparation de l’environnement
Avant de commencer, créez un projet uv dédié à cette séance. Dans un terminal, placez-vous dans le dossier de la séance (par exemple imt/s5/info/prog/session1), puis lancez la commande suivante :
Vous écrirez tous les fichiers de cette séance dans ce projet, en ajoutant les bibliothèques dont vous avez besoin avec uv add et en exécutant vos programmes dans l’environnement virtuel associé. Au besoin, revoyez la fiche Gestion moderne de projet Python avec uv.
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 de caractères
- 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(): efface le contenu du tableau.
Question. Dans un fichier blackboard.py, 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 l’effacer et, si le contenu du tableau n’est pas vide après l’avoir effacé, écrire un message d’erreur Error: The blackboard should be empty.
Un compte bancaire
Information importante
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.
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 bank.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 siamountest un nombre positif. Si ce n’est pas le cas, le dépôt n’est pas effectué et le message d’erreurError: The amount cannot be < 0.s’affiche sur la consolewithdraw(amount: float): effectue des retraits. Un retrait ne peut être possible que siamountest un nombre positif et que le compte a suffisamment d’argent pour couvrir le retrait. Si ce n’est pas le cas, le retrait n’est pas effectuéget_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 messages d’erreur sont affichés comme attendu.
Un compte bancaire et son propriétaire
Supposons que nous voulions savoir pour un compte bancaire qui est son propriétaire. Pour cela, vous devez :
- créer une classe
Personqui caractérise une personne avec sonfirstname,lastname,age. - modifier votre classe
BankAccountpour ajouter un attributownercomme unePersonqui 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éation de la classe
Person.
Dans le fichier bank.py ajoutez la nouvelle classe avant BankAccount.
- La classe aura les attributs suivants :
_firstname,_lastname, et_age. - 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_lastnameen majuscules.
- Une méthode
fullname(self) -> strretourne une chaîne correspondant à la concaténation des attributs_firstnameet_lastname.
- Une méthode
__str__(self) -> strretourne une représentation sous forme de chaîne de la personne, incluant son nom, prénom et son âge.
Placement de la définition de la classe dans le fichier
Si vous placez la définition de la classe Person après la classe BankAccount, les références à la classe Person dans BankAccount doivent être placées entre guillemets simples ('Person') pour différer leur évaluation. Cela permet de faire une référence en avant à ce type.
- 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_ownerde la classe avec la valeur du nouveau paramètre si celui-ci n’est pasNone(sinon, un message d’erreur est affiché sur la console). - La méthode
get_owner(self) -> Personretourne la valeur de l’attribut_owner. - Ajoutez les méthodes
__str__(self) -> stret__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 nom complet du 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.
- Utilisation des classes. Créez une personne, puis deux comptes bancaires dont elle est propriétaire. Effectuez un dépôt sur un des comptes, puis affichez le compte : vérifiez que les informations affichées sont correctes.
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 eux-mêmes des personnes). Un élu peut embaucher ou licencier un assistant.
Pour cela, vous allez créer une nouvelle classe ElectedOfficial qui représente les élus. Puisqu’un élu est une personne (Person), plutôt que de réécrire dans ElectedOfficial tout ce que fait dans Person, elle va hériter de Person et ajouter uniquement ce qui lui est propre.
Ajoutez la classe ElectedOfficial à la suite de Person et BankAccount, dans le même fichier bank.py. Un représentant élu :
-
est une personne avec un nouvel attribut
_assistantspour stocker ses assistants. A la création d’un élu, il n’a pas d’assistants -
a 3 nouvelles méthodes
hire_assistant(self, assistant: Person) -> Nonequi ajouteassistantà la liste des assistants s’il n’y est pas déjàfire_assistant(self, assistant: Person) -> Nonequi supprimeassistantde la liste des assistants s’il existeget_assistants(self)-> list[Person]qui retourne la liste des assistants
Question. Créez la classe ElectedOfficial comme décrit ci-dessus.
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 deux personnes. L’élu embauche la première comme assistante, affiche l’élu (vérifiez le nombre d’assistants), puis licencie cette assistante et affiche à nouveau l’élu.
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
Retrouver les comptes d’une personne
Pour l’instant, un compte connaît son propriétaire, mais une personne ne connaît pas ses comptes. Nous allons ajouter ce second sens à la relation, dans la classe Person.
- Ajoutez à
Personun attribut_accounts, une liste deBankAccount, vide à la création. - Ajoutez
add_account(self, account: 'BankAccount') -> None: ajouteaccounts’il n’est pas déjà dans la liste. - Ajoutez
remove_account(self, account: 'BankAccount') -> None: retireaccounts’il y est. - Ajoutez
get_accounts(self) -> list['BankAccount']: retourne la liste des comptes. - Mettez à jour le
__str__dePersonpour afficher le nombre de comptes. - Enfin, modifiez le constructeur de
BankAccountpour qu’à la création d’un compte, celui-ci s’ajoute automatiquement à la liste des comptes de son propriétaire.
Pourquoi des guillemets autour de 'BankAccount' ?
Dans Person, on annote le type des comptes par list['BankAccount'], avec BankAccount entre guillemets. En effet, Person est écrite avant BankAccount dans le fichier : au moment où Python lit la classe Person, le nom BankAccount n’existe pas encore. Les guillemets transforment l’annotation en simple texte, dont l’évaluation est différée : c’est ce qu’on appelle une référence en avant (forward reference). Le code n’utilisant BankAccount que comme annotation dans Person, cette astuce suffit — pas besoin d’import.
Question. Créez une personne et deux comptes à son nom. Affichez la personne : elle doit indiquer 2 comptes. Retirez un des comptes puis affichez à nouveau.
Distribuer un budget (spend_allocation)
Un élu peut distribuer une somme équitablement entre ses assistants. Ajoutez à ElectedOfficial la méthode :
spend_allocation(self, amount: float) -> dict[str, float]
- Si
amountest négatif, aucune distribution n’est faite et un message d’erreur est affiché. - Sinon, la somme est divisée équitablement entre les assistants. Pour chaque assistant qui possède au moins un compte, la part est déposée sur celui dont le solde est le plus faible.
- La méthode retourne un dictionnaire
{nom complet: part}pour les assistants qui n’ont aucun compte (leur argent devra leur être remis autrement).
Trouver le minimum d’une liste d’objets
Pour trouver le compte au solde le plus faible, on réutilise le motif classique de
recherche du minimum : on suppose que le premier élément est le minimum, puis on
parcourt la liste en gardant à jour ce minimum. La seule différence avec une liste
de nombres est qu’on ne compare pas les objets directement, mais la valeur renvoyée
par leur méthode get_balance().
Question. Créez un élu et trois assistants : un sans compte, un avec un compte, un avec deux comptes de soldes différents. Embauchez-les, distribuez un budget, puis affichez le solde de chaque compte et le dictionnaire retourné.
Séparer ElectedOfficial dans son propre fichier
Jusqu’ici, tout tient dans bank.py. Dans un vrai projet, on peut préférer avoir un fichier par classe (ou par groupe cohérent de classes). Déplaçons ElectedOfficial dans un fichier elected.py.
Comme ElectedOfficial hérite de Person et manipule des Person, elected.py doit importer ce dont il a besoin depuis bank.py :
Quand faut-il vraiment gérer les références croisées ?
Séparer les fichiers peut réintroduire un problème que le fichier unique nous évitait. Ici, elected.py dépend de bank.py, mais bank.py ne dépend pas de elected.py : la dépendance va dans un seul sens, tout se passe bien avec un simple from bank import Person.
Le problème n’apparaîtrait que si deux fichiers dépendaient l’un de l’autre (par exemple person.py important BankAccount et bankaccount.py important Person). On parle alors d’import circulaire, et Python lève une erreur au chargement. La solution consiste à différer l’import réservé au typage :
TYPE_CHECKING vaut True uniquement pendant la vérification statique de type (avec un outil comme MyPy ou Pylance), et False à l’exécution : l’import n’a donc lieu que pour le typage, jamais au moment où le programme tourne, ce qui casse le cycle. Vous n’en avez pas besoin dans cette extension — c’est une astuce à connaître pour le jour où vous découperez deux classes mutuellement dépendantes.
Pour aller encore plus loin
Comparer deux personnes avec __eq__
Par défaut, == entre deux objets teste s’il s’agit du même objet en mémoire (la même
identité), et non s’ils se « ressemblent ». Nous voulons plutôt considérer que deux personnes
sont égales si elles ont le même prénom, le même nom et le même âge.
- Ajoutez à
Personune méthode__eq__(self, other: object) -> boolqui retourneTruesiotherest unePersonayant les mêmes_firstname,_lastnameet_age, etFalsesinon. Pensez à vérifier d’abord, avecisinstance, queotherest bien unePerson.
Un point de vigilance
Redéfinir __eq__ sur Person ne change que la comparaison entre Person. Vos méthodes
add_account et remove_account continuent de fonctionner : le in / not in qu’elles
utilisent compare des objets BankAccount, qui n’ont pas de __eq__ et sont donc toujours
comparés par identité.
En revanche, une classe qui définit __eq__ sans définir __hash__ devient non hachable :
on ne peut plus placer une Person dans un set ni l’utiliser comme clé de dictionnaire.
Ce n’est pas gênant ici, mais c’est bon à savoir.
Question. Créez une personne alice et un compte à son nom. Créez ensuite une seconde
personne new_alice avec exactement les mêmes prénom, nom et âge. Comparez alice et
new_alice avec ==. Comparez aussi account.get_owner() avec new_alice. Enfin, affichez
les comptes de new_alice. Que constatez-vous ? Sauriez-vous l’expliquer ?
Règles de bonnes pratiques
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 :