Activité pratique

Durée2h30

Consignes 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 :

​
uv init --no-package
uv init --no-package
uv init --no-package
uv init --no-package

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 quatre 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() : efface le contenu du tableau.
À faire

Question. Dans un fichier blackboard.py, créez la classe Blackboard comme décrit ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​

Dans la solution, nous choisissons d’utiliser l’attribut __surface pour stocker le contenu du tableau afin que la modification directe du contenu ne soit pas possible.

class Blackboard:
  """ Represents a blackboard with a surface that can be written on, read and erase.
  """
  def __init__(self) -> None:
    """
    Initializes a new instance of the Blackboard class.
    """
    self.__surface: str = ""

  def write(self, text: str) -> None:
    """
    Writes the specified text on the surface of the blackboard.

    Parameters:
    -----------
    text: str
        The text to be written on the surface of the blackboard.
    """
    if text != "":
        # Add a new line if the text is not empty
        self.__surface += '\n'

    self.__surface += text

  def read(self) -> str:
    """
    Returns the text written on the surface of the blackboard.
    """
    return self.__surface

  def show(self) -> None:
    """
    Shows the text written on the surface of the blackboard.
    """
    print(self.__surface)

  def erase(self) -> None:
    """
    Erases the text written on the surface of the blackboard.
    """
    if self.__surface != "":
        self.__surface = ""
À faire

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.

Correction
Cliquez ici pour afficher la correction.
​
if __name__ == "__main__":
  one_blackboard = Blackboard()
  one_blackboard.write("Hello world")
  one_blackboard.show()
  one_blackboard.erase()
  if one_blackboard.read() != "":
    print("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.

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 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 si amount est un nombre positif. Si ce n’est pas le cas, le dépôt n’est pas effectué et le message d’erreur Error: The amount cannot be < 0. s’affiche sur la console
    • withdraw(amount: float) : 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, le retrait n’est pas effectué
    • get_account_number() -> int : retourne le numéro de compte
    • get_transaction_history() -> list[float] : retourne l’ensemble des transactions effectuées sur le compte
    • get_balance()-> float : retourne le solde actuel.
À faire

Question. Créez la classe BankAccount comme décrit ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​
class BankAccount:
    """
    Represents a bank account with a balance and a transaction history.
    """
    def __init__(self, account_number: int) -> None:
        """
        Initializes a new instance of the BankAccount class.

        Parameters:
        -----------
        account_number: int
            The account number.
        """
        self._account_number: int = account_number
        self._balance: float = 0.0
        self._history: list[float] = []

    def deposit(self, amount: float) -> None:
        """
        Deposits money into the bank account.

        Parameters:
        -----------
        amount: int
            The amount to be deposited into the account.
        
        """
        if amount <= 0:
            print("Error: the amount cannot be > 0")
            return
        else:
            self._balance += amount
            self._history.append(amount)

    def withdraw(self, amount: float) -> None:
        """
        Withdraws money from the bank account.

        Parameters:
        -----------
        amount: float
            The amount to be withdrawn from the account.
        """
        if 0 < amount <= self._balance:
            self._balance -= amount
            self._history.append(-amount)
        else:
            return

    def get_account_number(self) -> int:
        """
        Returns the account number of the bank account.
        """
        return self._account_number

    def get_balance(self) -> float:
        """
        Returns the balance of the bank account.

        Returns:
        --------
        The balance of the bank account.
        """
        return self._balance

    def get_transaction_history(self) -> list[float]:
        """
        Returns the transaction history of the bank account.

        Returns:
        --------
        The transaction history of the bank account.
        """
        return self._history
À faire

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.

Correction
Cliquez ici pour afficher la correction.
​
if __name__ == "__main__":
    my_account = BankAccount(1)
    my_account.deposit(500)
    my_account.deposit(200)
    my_account.deposit(-50) # Amount not deposited
    my_account.withdraw(10000) # Amount not withdrawn
    my_account.withdraw(-50) # Amount not withdrawn. Should show an error message
    my_account.withdraw(100)
    print("Transaction history:", my_account.get_transaction_history()) # Should give [500, 200, -100]
    print("Balance:", my_account.get_balance()) # Should give 600

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 Person qui caractérise une personne avec son firstname, lastname, age.
  • 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) est levée). De plus, une méthode get_owner() doit être fournie par la classe.
  1. 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, firstname: str, lastname: str, age: int). Il initialise tous les attributs de la classe, avec l’attribut _lastname en majuscules.
  • Une méthode fullname(self) -> str retourne une chaîne correspondant à la concaténation des attributs _firstname et _lastname.
  • Une méthode __str__(self) -> str retourne 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.

À faire

Répondez à la question ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​
class Person:
    """Represents a person with a name, firstname, and age."""

    def __init__(self, firstname: str, lastname: str, age: int) -> None:  # The constructor
        """Constructs a new Person object with the specified firstname, lastname, and age."""
        self._lastname: str = lastname.upper()
        self._firstname: str = firstname
        self._age: int = age

    def fullname(self) -> str:
        return f"{self._firstname} {self._lastname}"

    def __str__(self) -> str:
        """Returns a string representation of the Person object."""
        return f"{self._firstname} {self._lastname} ({self._age} old)."
  1. Modifications de la classe BankAccount.
À faire
  • 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 pas None (sinon, un message d’erreur est affiché sur la console).
  • 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 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.
Correction
Cliquez ici pour afficher la correction.
​
class BankAccount:
    """
    Represents a bank account with a number, an owner, a balance, and a transaction history.
    """

    def __init__(self, account_number: int, owner: Person) -> None:
        """
        Initializes a new instance of the BankAccount class.

        Parameters:
        -----------
        account_number: int
            The account number.
        owner: Person
            The owner of the account.
        """
        if owner is None:
            print("Error: the owner cannot be None.")
            return
        self._account_number: int = account_number
        self._owner: Person = owner
        self._balance: float = 0
        self._history: list[float] = []

    def deposit(self, amount: float) -> None:
        """
        Deposits the specified amount to the balance of the bank account.

        Parameters:
        -----------
        amount: float
            The amount to deposit.
        """
        if amount < 0:
            print("Error: the amount cannot be > 0")
            return
        self._balance += amount
        self._history.append(amount)

    def withdraw(self, amount: float) -> None:
        """
        Withdraws the specified amount from the balance of the bank account.

        Parameters:
        -----------
        amount: float
            The amount to withdraw.
        """
        if 0 < amount <= self._balance:
            self._balance -= amount
            self._history.append(-amount)
        else:
            return

    def get_account_number(self) -> int:
        """
        Returns the account number of the bank account.
        """
        return self._account_number

    def get_balance(self) -> float:
        """
        Returns the balance of the bank account.

        Returns:
        --------
        The balance of the bank account.
        """
        return self._balance

    def get_transaction_history(self) -> list[float]:
        """
        Returns the transaction history of the bank account.

        Returns:
        --------
        The transaction history of the bank account.
        """
        return self._history

    def get_owner(self) -> 'Person':
        """
        Returns the owner of the bank account.

        Returns:
        --------
        The owner of the bank account.
        """
        return self._owner

    def __str__(self) -> str:
        return f"Account number {self._account_number} from {self._owner.fullname()} has {self._balance} € and has done {len(self._history)} transactions"

    def __repr__(self) -> str:
        return f"Account number {self._account_number}: {self._owner.fullname()} has {self._balance} € and has done {len(self._history)} transactions"
  1. 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.
À faire
Correction
Cliquez ici pour afficher la correction.
​
if __name__ == "__main__":

    # Create a person
    alice_weber = Person("Alice", "Weber", 33)
    print(alice_weber) 

    # Create three bank accounts for Alice
    alice_account = BankAccount(1, alice_weber)

    # Make some transactions
    alice_account.deposit(500)
    print(alice_account)

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 _assistants pour 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) -> None qui ajoute assistant à la liste des assistants s’il n’y est pas déjà
    • fire_assistant(self, assistant: Person) -> None qui supprime assistant de la liste des assistants s’il existe
    • get_assistants(self)-> list[Person] qui retourne la liste des assistants
À faire

Question. Créez la classe ElectedOfficial comme décrit ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​
class ElectedOfficial(Person):
    """
    Represents an elected official: a person with a list of assistants.
    It inherits from the Person class.
    """

    def __init__(self, name: str, firstname: str, age: int) -> None:
        """
        Constructs a new ElectedOfficial. He/she has no assistant yet.
        """
        super().__init__(name, firstname, age)  # Call the parent constructor
        self._assistants: list[Person] = []

    def hire_assistant(self, assistant: Person) -> None:
        """
        Adds the assistant to the list if it is not already in it.
        """
        if assistant not in self._assistants:
            self._assistants.append(assistant)

    def fire_assistant(self, assistant: Person) -> None:
        """
        Removes the assistant from the list if it is in it.
        """
        if assistant in self._assistants:
            self._assistants.remove(assistant)

    def get_assistants(self) -> list[Person]:
        """
        Returns the list of assistants.
        """
        return self._assistants
À faire

Question. Ajoutez une méthode __str__() pour décrire votre nouvelle classe. Pour cela, redéfinissez la méthode __str__() de la classe parente.

Correction
Cliquez ici pour afficher la correction.
​
class ElectedOfficial(Person):
    """
    Represents an elected official with a list of assistants.
    It inherits from the Person class.
    """

    # ... (rest of the class as above) ...
    
    def __str__(self) -> str:
        """
        Returns a string representation, reusing the parent's __str__.
        """
        return super().__str__() + f" and {len(self._assistants)} assistant(s)"
À faire

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.

Correction
Cliquez ici pour afficher la correction.
​
if __name__ == "__main__":
    boss = ElectedOfficial("Ducas", "Michael", 45)
    print(boss)                       # ... has 0 bank account? -> voir remarque ci-dessous

    david = Person("David", "Landon", 25)
    linda = Person("Linda", "Lucas", 30)

    boss.hire_assistant(david)
    boss.hire_assistant(linda)
    print(boss)                       # ... and 2 assistant(s)

    boss.fire_assistant(david)
    print(boss)                       # ... and 1 assistant(s)

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 à Person un attribut _accounts, une liste de BankAccount, vide à la création.
  • Ajoutez add_account(self, account: 'BankAccount') -> None : ajoute account s’il n’est pas déjà dans la liste.
  • Ajoutez remove_account(self, account: 'BankAccount') -> None : retire account s’il y est.
  • Ajoutez get_accounts(self) -> list['BankAccount'] : retourne la liste des comptes.
  • Mettez à jour le __str__ de Person pour afficher le nombre de comptes.
  • Enfin, modifiez le constructeur de BankAccount pour 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.

À faire

Répondez à la question ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​
class Person:
    """
    Represents a person with a firstname, a lastname, an age
    and the list of bank accounts he/she owns.
    """

    def __init__(self, name: str, firstname: str, age: int) -> None:
        self._lastname: str = name.upper()
        self._firstname: str = firstname
        self._age: int = age
        self._accounts: list['BankAccount'] = []

    def fullname(self) -> str:
        return f"{self._firstname} {self._lastname}"

    def add_account(self, account: 'BankAccount') -> None:
        """Adds the account to the list if it is not already present."""
        if account not in self._accounts:
            self._accounts.append(account)

    def remove_account(self, account: 'BankAccount') -> None:
        """Removes the account from the list if it is present."""
        if account in self._accounts:
            self._accounts.remove(account)

    def get_accounts(self) -> list['BankAccount']:
        """Returns the list of the person's bank accounts."""
        return self._accounts

    def __str__(self) -> str:
        return (
            f"{self._firstname} {self._lastname} ({self._age} years old) "
            f"has {len(self._accounts)} bank account(s)"
        )

# In BankAccount.__init__, add the following line at the end of the constructor:
#     owner.add_account(self)
À faire

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.

Correction
Cliquez ici pour afficher la correction.
​
if __name__ == "__main__":
    alice = Person("Alice", "Weber", 33)
    account1 = BankAccount(1, alice)   # registers itself with alice
    account2 = BankAccount(2, alice)   # registers itself with alice
    print(alice)                       # ... has 2 bank account(s)

    alice.remove_account(account2)
    print(alice)                       # ... has 1 bank account(s)

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 amount est 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().

À faire

Répondez à la question ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​
    def spend_allocation(self, amount: float) -> dict[str, float]:
        """
        Distributes `amount` equally among the assistants.
        For an assistant who owns at least one account, the share is deposited
        on the account with the lowest balance.

        Returns:
        --------
        A dict {full name: share} for assistants who have no account.
        """
        if amount < 0:
            raise ValueError("The amount to distribute must be positive")
        if len(self._assistants) == 0:
            return {}

        share = amount / len(self._assistants)
        no_account: dict[str, float] = {}
        for assistant in self._assistants:
            accounts = assistant.get_accounts()
            if len(accounts) == 0:
                no_account[assistant.fullname()] = share
            else:
                # Find the account with the lowest balance
                min_account = accounts[0]
                for account in accounts:
                    if account.get_balance() < min_account.get_balance():
                        min_account = account
                min_account.deposit(share)
        return no_account
À faire

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

Correction
Cliquez ici pour afficher la correction.
​
if __name__ == "__main__":
    boss = ElectedOfficial("Ducas", "Michael", 45)

    # No account
    david = Person("David", "Landon", 25)
    # One account
    linda = Person("Linda", "Lucas", 30)
    BankAccount(1, linda)
    # Two accounts with different balances
    yann = Person( "Yann", "Breizh", 28)
    BankAccount(2, yann)
    yann_second = BankAccount(3, yann)
    yann_second.deposit(200)

    boss.hire_assistant(david)
    boss.hire_assistant(linda)
    boss.hire_assistant(yann)

    leftover = boss.spend_allocation(300)
    print("To be given by other means:", leftover)  # {'David LANDON': 100.0}

    for assistant in boss.get_assistants():
        for account in assistant.get_accounts():
            print(account)

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 :

from bank import Person   # BankAccount n'est pas nécessaire ici : on ne l'utilise pas directement
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 :

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from bankaccount import BankAccount

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.

À faire

Répondez à la question ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​

bank.py ne contient plus que Person et BankAccount. Le fichier elected.py :

from bank import Person


class ElectedOfficial(Person):
    """
    Represents an elected official: a person with a list of assistants.
    """

    def __init__(self, name: str, firstname: str, age: int) -> None:
        super().__init__(name, firstname, age)
        self._assistants: list[Person] = []

    # ... hire_assistant, fire_assistant, get_assistants,
    #     __str__ and spend_allocation, unchanged ...


if __name__ == "__main__":
    from bank import BankAccount   # needed only in the test scenario

    boss = ElectedOfficial("Ducas", "Michael", 45)
    linda = Person("Linda", "Lucas", 30)
    BankAccount(1, linda)
    boss.hire_assistant(linda)
    print(boss.spend_allocation(300))

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 à Person une méthode __eq__(self, other: object) -> bool qui retourne True si other est une Person ayant les mêmes _firstname, _lastname et _age, et False sinon. Pensez à vérifier d’abord, avec isinstance, que other est bien une Person.
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 ?

À faire

Répondez à la question ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​

​```python def eq(self, other: object) -> bool: """ Two persons are equal if they have the same firstname, lastname and age.

    Parameters:
    -----------
    other: object
        The object to compare with.
Returns:
--------
bool: True if both are Person objects with the same attributes, False otherwise.
&quot;&quot;&quot;
if not isinstance(other, Person):
    return False
return (
    self._firstname == other._firstname
    and self._lastname == other._lastname
    and self._age == other._age
)

​```

À faire

Répondez à la question ci-dessus.

Correction — le scénario de test
Cliquez ici pour afficher la correction.
​

​```python if name == “main”: alice = Person(“Alice”, “Weber”, 33) account = BankAccount(1, alice)

new_alice = Person("Alice", "Weber", 33)

print(alice == new_alice) # True: same attributes print(alice is new_alice) # False: two different objects print(account.get_owner() == new_alice) # True print(new_alice.get_accounts()) # [] : new_alice owns nothing

​```

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 :

  • Le guide de style officiel de Python, dont les conventions de nommage des classes, des méthodes et des attributs.