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.

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 = ""

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.

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

2 - 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 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 si amount est un nombre positif. Si ce n’est pas le cas, une exception (ValueError) doit être levée
    • 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, une exception (ValueError) doit être levée
    • 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.

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

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.
        Raises:
        -------
        ValueError: If the amount is not positive.
        """
        if amount <= 0:
            raise ValueError("Amount must be positive")
        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.
        Raises:
        -------
        ValueError: If the amount is not positive or greater than the balance.
        """
        if amount <= 0:
            raise ValueError("Amount must be positive")
        elif amount > self._balance:
            raise ValueError("Amount must be less than the balance")
        else:
            self._balance -= amount
            self._history.append(-amount)

    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

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.

Correction
if __name__ == "__main__":
    my_account = BankAccount(1)
    my_account.deposit(500)
    my_account.deposit(200)
    try:
        my_account.deposit(-50)
    except ValueError as e:
        print("ERROR", e)
    try:
        my_account.withdraw(10000)
    except ValueError as e:
        print("ERROR", e)
    try:
        my_account.withdraw(-50)
    except ValueError as e:
        print("ERROR", e)

    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

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) est levée). De plus, une méthode get_owner() 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. 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 pas None (sinon, une exception ValueError 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.
Correction
from person import Person


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. Adds the account to the owner's list of accounts.

        Parameters:
        -----------
        account_number: int
            The account number.
        owner: Person
            The owner of the account.
        Raises:
        -------
        ValueError: If the owner is None.
        """
        if owner is None:
            raise ValueError("Owner must be specified")
        self._account_number: int = account_number
        self._owner: Person = owner
        self._balance: float = 0
        self._history: list[float] = []
        # The owner adds the new account to his/her list of accounts
        owner.add_account(self)

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

        Parameters:
        -----------
        amount: float
            The amount to deposit.
        Raises:
        -------
        ValueError: If the amount is not positive.
        """
        if amount < 0:
            raise ValueError("Amount must be positive")
        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.
        Raises:
        -------
        ValueError: If the amount is not positive or greater than the balance.
        """
        if 0 < amount <= self._balance:
            self._balance -= amount
            self._history.append(-amount)
        else:
            raise ValueError("Amount must be positive and less than the balance")

    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. 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 ajoute account à 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 retire account 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 retourne True 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.
Avertissement

Les références à la classe BankAccount sont placées entre guillemets simples ('BankAccount') pour différer leur évaluation. Cela permet de faire une référence en avant à ce type et éviter les erreurs de référence cyclique, car la classe Person fait référence à la classe BankAccount, et vice versa.

D’autre part, si vous avez installer l’extension (Pylance) dans VSCode, vous pouvez avoir des messages (warnings) du type « BankAccount » n’est pas défini (Pylance). Pour corriger cela, ajoutez les lignes suivantes au début de votre fichier person.py :

from typing import *
if TYPE_CHECKING:
    from bankaccount import BankAccount

Ainsi, les imports sont différés à la phase de vérification de type.

Plus précisément :

  • la première ligne importe tout du module typing, qui fournit des outils pour le typage statique en Python.
  • 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.
Correction
from typing import *

if TYPE_CHECKING:
    from bankaccount import BankAccount


class Person:
    """Represents a person with a name, firstname, age and the list of bank accounts."""

    def __init__(self, name: str, firstname: str, age: int) -> None:  # The constructor
        """Constructs a new Person object with the specified lastname, firstname, age, and his/her accounts."""
        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 get_accounts(self) -> list['BankAccount']:
        """Returns the list of bank accounts of the person."""
        return self._accounts

    def add_account(self, account: 'BankAccount') -> None:
        """Adds the specified bank account to the list of bank accounts of the person if it is not already present.
        Args:
            account (BankAccount): The bank account to add.
        """

        if account not in self._accounts:
            self._accounts.append(account)

    def remove_account(self, account: 'BankAccount') -> None:
        """Removes the specified bank account from the list of bank accounts of the person.
        Args:
            account (BankAccount): The bank account to remove.
        """
        if account in self._accounts:
            self._accounts.remove(account)

    def __str__(self) -> str:
        """Returns a string representation of the Person object."""
        return f"{self._firstname} {self._lastname} ({self._age} old) has {len(self._accounts)} bank accounts"

    def __eq__(self, other: object) -> bool:
        """Compares two Person objects.
        Args:
            other (object): The object to compare with.
        Returns:
            bool: `True` if the objects are equal, `False` otherwise.
            Two persons are equal if they have the same lastname, firstname and age.
        """
        # Both objects should be instances of the same class (Person)
        if not isinstance(other, Person):
            return False
        return (
            self._firstname == other._firstname
            and self._lastname == other._lastname
            and self._age == other._age
        )
  1. Écrivez une fonction __main__. Dans un fichier main.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é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. 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 ?
Correction
from bankaccount import BankAccount
from person import Person

if __name__ == "__main__":

    # Create a person
    alice_weber = Person("Weber", "Alice", 33)
    print(alice_weber)  # Should give no bank account

    # Create three bank accounts for Alice
    alice_account1 = BankAccount(1, alice_weber)
    alice_account2 = BankAccount(2, alice_weber)
    alice_account3 = BankAccount(3, alice_weber)

    print(alice_weber)  # Should give three bank accounts

    # Make some transactions
    alice_account1.deposit(500)
    alice_account2.deposit(1000)
    print(alice_account1)  # Should give 500
    print(alice_account2)  # Should give 1000

    alice_account2.withdraw(30)
    print(alice_account2)  # Should give 970

    # Remove the second bank account from Alice's accounts
    alice_weber.remove_account(alice_account2)
    print(alice_weber)  # Should give two bank accounts

    # If there is no money in account 3 add some
    if alice_account3.get_balance() == 0:
        alice_account3.deposit(5000)
    print(alice_account3)  # Should give 5000 and 1 transaction

    # Show the balance of all accounts of alice
    str_elements = ""
    for element in alice_weber.get_accounts():
        str_elements += str(element) + "; "
    print(str_elements)

    # Optional. Test __eq__ method
    new_alice = Person("Weber", "Alice", 33)
    if alice_account1.get_owner() == new_alice:
        print("get_owner Success. new_alice and alice_weber are the same person")
    else:
        print("get_owner Failure. new_alice and alice_weber are not the same person")

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 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
    • spend_allocation(self, amount: float) -> dict[str, float] qui distribue équitablement amount parmi les assistants si le amount 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.

Correction
from person import Person


class ElectedOfficial(Person):
    """
    Represents an elected official with a list of assistants.
    It inherits from the Person class.
    """

    def __init__(self, name: str, firstname: str, age: int) -> None:  # The constructor
        """
        Constructs a new ElectedOfficial object with the specified lastname, firstname, age
        He/she has no assistants.

        Parameters:
        -----------
        name: str
            The lastname of the elected official.
        firstname: str
            The firstname of the elected official.
        age: int
            The age of the elected official.
        """
        super().__init__(
            name, firstname, age
        )  # Call the constructor of the parent class
        self._assistants: list[Person] = []

    def hire_assistant(self, assistant: Person) -> None:
        """
        Adds the specified assistant to the list of assistants of the elected official if it is not already in it.

        Parameters:
        -----------
        assistant: Person
            The assistant to add.
        """
        if assistant is not None:
            self._assistants.append(assistant)

    def fire_assistant(self, assistant: Person) -> None:
        """
        Removes the specified assistant from the list of assistants of the elected official if it exists.

        Parameters:
        -----------
        assistant: Person
            The assistant to remove.
        """
        if assistant in self._assistants:
            self._assistants.remove(assistant)

    def get_assistants(self) -> list[Person]:
        """
        Returns the list of assistants of the elected official.

        Returns:
        --------
        The list of assistants of the elected official.
        """
        return self._assistants

    def spend_allocation(self, amount: float) -> dict[str, float]:
        """
        Distributes the specified amount equally between the assistants of the elected official if the amount is positive.
        If an assistant has no bank account, he/she receives no money

        Parameters:
        -----------
        amount: float
            The amount to distribute.

        Returns: for each assistant with no bank account the amount to be given

        Raises:
        -------
        ValueError: if amount is < 0
        """
        if amount < 0:
            raise ValueError("The amount to distribute should be positive")

        result = {}
        for assistant in self._assistants:  # For each assistant
            assistant_accounts = assistant.get_accounts()
            if len(assistant_accounts) == 0:  # The assistant has no accounts yet
                result[assistant.fullname()] = amount / len(self._assistants)
            else:
                # Finding the account with the minimal balance. Option 1
                min_balance_account = min(
                    assistant_accounts, key=lambda account: account.get_balance()
                )
                # Finding the account with the minimal balance. Option 2
                # min_balance_account = assistant_accounts[0]
                # for account in assistant_accounts:
                #     if account.get_balance() < min_balance_account.get_balance():
                #         min_balance_account = account

                # Deposit the corresponding money
                min_balance_account.deposit(amount / len(self._assistants))
        return result

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
from person import Person


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 of the ElectedOfficial object.
        Uses the parent method to get a string representation of the ElectedOfficial object.
        """
        return super().__str__() + f" and {len(self._assistants)} assistants"

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.

Correction
if __name__ == "__main__":
    from bankaccount import BankAccount

    # Create an elected representative
    michael_ducas = ElectedOfficial("Michael", "Ducas", 33)
    print(
        f"Elected Representative {michael_ducas}\n"
    )  # Should give no bank account and no assistants

    # Create assistants of michael_ducas. No bank account
    david_landon = Person("David", "Landon", 25)

    # Create a second assistant. One bank account
    linda_lucas = Person("Lucas", "Linda", 30)
    linda_lucas.add_account(BankAccount(1, linda_lucas))

    # Create the third assistant. Two bank accounts
    yann_breizh = Person("Breizh", "Yann", 28)
    yann_breizh.add_account(BankAccount(2, yann_breizh))
    yann_account = BankAccount(3, yann_breizh)
    yann_breizh.add_account(yann_account)
    yann_account.deposit(200)

    # Hire an assistant
    michael_ducas.hire_assistant(david_landon)
    print(
        f"Elected Representative {michael_ducas}\n"
    )  # Should give no bank account and 1 assistant

    # Try to distribute 1000 euros: no distribution
    try:
        print(michael_ducas.spend_allocation(1000))
    except ValueError as e:
        print(e)
    # Show information on balance on assistants accounts
    for assistant in michael_ducas.get_assistants():
        assistant_accounts = assistant.get_accounts()
        for account in assistant_accounts:
            print(
                f"Assistant {assistant.fullname()}: account nber {account.get_account_number()} with balance {account.get_balance()}\n"
            )

    # Hire Linda Lucas
    michael_ducas.hire_assistant(linda_lucas)
    # Try to distribute 2000 euros: account transfer to Linda
    # Manual to David Landon
    try:
        print(michael_ducas.spend_allocation(2000))
    except ValueError as e:
        print(e)
    # Show information on balance on assistants accounts
    for assistant in michael_ducas.get_assistants():
        assistant_accounts = assistant.get_accounts()
        for account in assistant_accounts:
            print(
                f"Assistant {assistant.fullname()}: account nber {account.get_account_number()} with balance {account.get_balance()}\n"
            )

    # Hire Yann Breizh
    michael_ducas.hire_assistant(yann_breizh)
    # Try to distribute 1500 euros: account transfer to Linda and Yann (account 3)
    # Manual to David Landon
    try:
        print(michael_ducas.spend_allocation(2000))
    except ValueError as e:
        print(e)
    # Show information on balance on assistants accounts
    for assistant in michael_ducas.get_assistants():
        assistant_accounts = assistant.get_accounts()
        for account in assistant_accounts:
            print(
                f"Assistant {assistant.fullname()}: account nber {account.get_account_number()} with balance {account.get_balance()}\n"
            )

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

Correction
class RussianDoll:
    """
    Represents a Russian doll with a size, a state (opened or closed), a content and a container.
    """

    def __init__(self, size: int) -> None:
        """
        Initializes a new instance of the RussianDoll class.

        Parameters:
        -----------
        size: int
            The size of the doll.
        """
        self.size: int = size
        self._opened: bool = False
        self._content: RussianDoll | None = None
        self.placed_in: RussianDoll | None = None

    def open(self) -> None:
        """
        Opens the doll if it is not already open and if it is not inside another doll.
        """
        if not self._opened and self.placed_in is None:
            self._opened = True

    def close(self) -> None:
        """
        Closes the doll if it is not already closed and if it is not inside another doll.
        """
        if self._opened and self.placed_in is None:
            self._opened = False

    def place_in(self, p: 'RussianDoll') -> None:
        """
        Places the current doll in doll p, if possible.
        The current doll must be closed and not already inside another doll.
        p must be opened and contain no doll, and it must be larger than the current doll.

        Parameters:
        -----------
        p: RussianDoll
            The doll in which to place the current doll.
        """
        if (
            not self._opened
            and self.placed_in is None
            and p._opened
            and p._content is None
            and p.size > self.size
        ):
            self.placed_in = p
            p._content = self

    def get_out(self, p: 'RussianDoll') -> None:
        """
        Takes the current doll out of doll p if it's in p and if p is open.

        Parameters:
        -----------
        p: RussianDoll
            The doll from which to take out the current doll.
        """
        if self.placed_in == p and p._opened:
            self.placed_in = None
            p._content = None

    def __str__(self) -> str:
        """
        Returns a string representation of the doll, including its size and state.
        """
        state = "open" if self._opened else "closed"
        content = f"contains doll {self._content.size}" if self._content else "is empty"
        return f"Doll {self.size} is {state} and {content}"


def visualize_dolls(dolls: list['RussianDoll']) -> None:
    """
    Prints a visualization of all the Russian dolls and their relationships.

    Parameters:
    -----------
    dolls: list[RussianDoll]
        The list of Russian dolls to visualize.
    """
    for doll in dolls:
        container = (
            f"inside doll {doll.placed_in.size}"
            if doll.placed_in
            else "not inside any doll"
        )
        print(f"Doll: {doll} and is {container}.")


if __name__ == "__main__":
    # Create three russian dolls of different sizes
    russian_doll1 = RussianDoll(1)
    russian_doll2 = RussianDoll(2)
    russian_doll3 = RussianDoll(3)

    # Lets put dolls inside one another
    russian_doll3.open()
    russian_doll2.open()
    russian_doll1.place_in(russian_doll2)
    russian_doll2.close()
    russian_doll2.place_in(russian_doll3)

    # Visualize the relationships
    visualize_dolls([russian_doll1, russian_doll2, russian_doll3])

    russian_doll2.get_out(russian_doll3)
    # Visualize the relationships
    visualize_dolls([russian_doll1, russian_doll2, russian_doll3])

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.

Correction
class Ingredient:
    """
    Represents an ingredient with a name, quantity, state, unit and price.
    """
    def __init__(self, name: str, quantity: int, state: str, unit: str, price: float):
        """
        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).
        price: float
            The price of the ingredient.
        Raises:
        -------
        ValueError: If the quantity is negative, the unit is not valid, the state is not valid or the price is negative.
        """
        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'")
        if price < 0:
            raise ValueError("Price must be positive")

        self._name = name
        self._quantity = quantity
        self._unit = unit.lower()
        self._state = state.lower()
        self._price = price

    def __str__(self) -> str:
        """
        Returns a string representation of the Ingredient object.
        """
        return f"{self._quantity}{self._unit} {self._name} ({self._state}, {self._price}€)"

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.

Correction
class Meal:
    """
    Represents a meal with a name and a list of ingredients.
    """

    def __init__(self, name: str, ingredients: list[Ingredient] = []) -> None:
        """
        Initializes a new instance of the Meal class.

        Parameters:
        -----------
        name: str
            The name of the meal.
        ingredients: List[Ingredient]
            The list of ingredients of the meal, may be empty.
        """
        self._name: str = name
        self._ingredients: list[Ingredient] = ingredients

    def add_ingredient(self, ingredient: Ingredient):
        """
        Adds an ingredient to the meal.

        Parameters:
        -----------
        ingredient: Ingredient
            The ingredient to add.
        """
        self._ingredients.append(ingredient)

    def remove_ingredient(self, ingredient: Ingredient):
        """
        Removes an ingredient from the meal.

        Parameters:
        -----------
        ingredient: Ingredient
            The ingredient to remove.
        """
        self._ingredients.remove(ingredient)

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€)
Correction
class Meal:
    """
    Represents a meal with a name and a list of ingredients.
    """

    # ... (rest of the class as above) ...

    def __str__(self) -> str:
        """
        Returns a string representation of the Meal object.
        """
        nl = "\n- "
        return (
            f"{self._name} - {sum(ing._price for ing in self._ingredients)}€"
            f"{nl}{nl.join(str(ing) for ing in self._ingredients)}"
        )

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.

Correction
if __name__ == "__main__":
    # Pizza Margherita
    pizza_dough = Ingredient("Pizza Dough", 260, "raw", "g", 1.15)
    tomato_sauce = Ingredient("Tomato Sauce", 200, "cooked", "g", 2.27)
    mozzarella = Ingredient("Mozzarella", 125, "raw", "g", 1.19)
    parmigiano = Ingredient("Parmigiano Reggiano", 60, "raw", "g", 1.83)
    basil = Ingredient("Fresh Basil", 30, "raw", "g", 1.4)

    pizza = Meal("Pizza Margherita")
    pizza.add_ingredient(pizza_dough)
    pizza.add_ingredient(tomato_sauce)
    pizza.add_ingredient(mozzarella)
    pizza.add_ingredient(parmigiano)
    pizza.add_ingredient(basil)

    print(pizza)

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.

Correction
class Ingredient:
    """
    Represents an ingredient with a name, quantity, state, unit and price.
    """
    
    # ... (rest of the class as above) ...

    def __eq__(self, other: 'Ingredient') -> bool:
        """
        Compares two Ingredient objects.
        Two ingredients are equal if they have the same name and state.

        Parameters:
        -----------
        other: Ingredient
            The ingredient to compare with.

        Returns:
        --------
        bool: True if the ingredients are equal, False otherwise.
        """
        return self._name == other._name and self._state == other._state
class Meal:
    """
    Represents a meal with a name and a list of ingredients.
    """

    # ... (rest of the class as above) ...

    def __eq__(self, other: 'Meal') -> bool:
        """
        Compares two Meal objects.
        Two meals are equal if they contain the same ingredients.

        Parameters:
        -----------
        other: Meal
            The meal to compare with.

        Returns:
        --------
        bool: True if the meals are equal, False otherwise.
        """
        return set(self._ingredients) == set(other._ingredients)

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 :