POO, documentation et tests - TP5

Classes et diagrammes UML
Author

Ludovic Deneuville

1 Créer l’arborscence

└── P
    └── Cours1A
        └── IPOO
            ├── TP1
            │   ├── Exercice_1
            │   │   └── ex.py
            │   ├── Exercice_2
            │   │   └── ex.py
            │   ├── ...
            │   └── Exercice_7
            │       └── ex.py
            ├── TP2
            │   ├── Exercice_1
            │   │   └── ex.py
            │   ├── Exercice_2
            │   ├── ...

Pour prendre la bonne habitude de bien ranger vos fichiers, vous allez créer l’arborscence ci-dessus. Vous pouvez créer chaque dossier et fichier à la main, ou utiliser les commandes ci-dessous (mkdir : make directory):

    • Sur le bureau > clic droit > Git Bash here
    • mkdir -p /p/Cours1A/IPOO/TP5/Exercice_{1..5}/
    • dans Git Bash : clic droit > Paste ou MAJ + INSER
Utilisez le lecteur P:

Il est important de stocker vos fichiers dans votre lecteur P: et pas sur votre bureau ou votre disque C: car vos fichiers risqueraient de disparaitre !

2 Moodle

3 Rappels

3.1 Classes et Objets

  • une classe est comme un “moule” qui sert à fabriquer des objets
  • un objet est une instance de classe
  • une classe est composée
    • d’attributs : ce qu’elle est
    • de méthodes : ce qu’elle peut faire
Important
  • un nom de classe s’écrit en CamelCase (majuscule à chaque mot)
  • une seule classe par fichier
    • une classe = un module
    • le nom du module s’écrit en snake_case (mots en minuscules séparés par des _)

Exemple : classe Frac

frac.py
class Frac:
    def __init__(self, num, denom=1) -> None:
        """Constructeur"""        
        if denom == 0:
            raise ValueError("le dénominateur ne peut être nul")
        self.num = num 
        self.denom = denom

    def add(self, autre_fraction):
        """Additionne deux fractions"""
        numerateur = self.num * autre_fraction.denom + autre_fraction.num * self.denom 
        denominateur = self.denom * autre_fraction.denom 
        return Frac(numerateur, denominateur)

    def __str__(self) -> str:
        """Retourne une chaîne affichant la fraction"""
        return f"{self.num} / {self.denom}"

Exemple : Créer des objets Frac

__main__.py
from frac import Frac

f1 = Frac(1, 2)
f2 = Frac(1, 4)

f3 = f1.add(f2)
print(f3)

3.2 Tests

Doctest

  • écrits dans la documentation sous le mot clé Examples
  • aprés les >>> : code Python exécuté
    • si le code renvoie un résultat, écrire ce résultat à la ligne en dessous
    • sinon, écrire le code suivant

Reprenons la classe Frac ci-dessus

frac.py
class Frac:
    def __init__(self, num, denom=1) -> None:
        """Constructeur

        Examples
        --------        
        >>> f1 = Frac(1, 2)
        >>> f1.num, f1.denom
        (1, 2)
        >>> f2 = Frac(3)
        >>> f2.num, f2.denom
        (3, 0)
        >>> f4 = Frac(4, 0)
        Traceback (most recent call last):
        ...
        ValueError: le dénominateur ne peut être nul
        """        
        if denom == 0:
            raise ValueError("le dénominateur ne peut être nul")
        self.num = num 
        self.denom = denom

    def add(self, autre_fraction):
        """Additionne deux fractions
        
        Examples
        --------
        >>> f1 = Frac(1, 2)
        >>> f2 = Frac(1, 3)
        >>> f3 = f1.add(f2)
        >>> f3.num
        5
        >>> f3.denom
        6
        """
        numerateur = self.num * autre_fraction.denom + autre_fraction.num * self.denom 
        denominateur = self.denom * autre_fraction.denom 
        return Frac(numerateur, denominateur)

Lancer les tests

Plusieurs possibilités :

  1. Coller ceci en bas de votre classe, puis cliquer sur l’icone éxéctuer ▶️
if __name__ == '__main__':
    import doctest    

    doctest.testmod(verbose=True)
  1. dans un terminal : python -m pytest --doctest-modules
  2. utiliser l’interface de VSCode (à gauche icone Testing en forme de fiole triangulaire)

4 Exercices

4.1 Exercice 1

      • dans votre Explorer à gauche apparait l’arborescence des dossiers et fichiers de Exercice_1

Attributs privés

En rendant les attributs de classe privés, vous pouvez contrôler strictement l’accès à ces attributs.
Vous pouvez fournir des méthodes spécifiques : “getter” et “setter”, pour manipuler ces attributs de manière contrôlée et sûre.

Exemple :

Public

Avec un attribut public, aucun controle n’est effectué sur prix.
Il est alors possible à l’extérieur de l’objet de :

  • obtenir sa valeur
  • modifier sa valeur, et même son type
article.py
class Article:
    def __init__(self, prix):
        self.prix = prix
main.py
a = Article(5)

a.prix = "toto"
a.prix = True

print(a.prix)
# True
Privé

Avec un attribut privé, il est possible de mettre en place des controles

article.py
class Article:
    def __init__(self, prix):
        self.__prix = prix

    def obtenir_prix(self):
        '''Getter'''
        return self.__prix

    def modifier_prix(self, new_prix):
        '''Setter'''
        if isinstance(new_prix, int) and new_prix > 0:
            self.__prix = new_prix
main.py
a = Article(5)

# print(a.__prix)
# 'Article' object has no attribute '__prix'

a.modifier_prix(10)
print(a.obtenir_prix())
# 10

Quand vous déclarez un attribut privé dans votre constructeur, Python va renommer votre attribut en ajoutant un préfixe _ClassName

Dans l’exemple ci-dessus, l’attribut __prix deviendra _Article__prix.

a = Article(5)
print(a._Article__prix)
# 5

Si vous tentez de modifier l’attribut __prix, cela va en fait créer un nouvel attribut

a = Article(5)
a.__prix = 10
print(a.__prix)
# 10
print(a._Article__prix)
# 5
Public, privé, protected
Code UML Préfixe Python Description
Public + Par défaut, tous les attributs et méthodes dans une classe Python sont considérés comme publics. Cela signifie qu’ils peuvent être accédés depuis l’extérieur de la classe.
Privé - __ Pas natif en Python. Il ne peut être accédé qu’à l’intérieur de la classe elle-même.
Protected # _ Pas natif en Python. Seules la classe possédant l’élément et les classes filles peuvent y accéder.

4.2 Exercice 2

    • correction UML : la méthode remplir() renvoie un booléen
    • Pour celles et ceux qui veulent tenter de nouvelles expériences, écrivez les doctests avant de coder vos fonctions
  • méthode remplir() : pensez à bien effectuer toutes les vérifications
  • méthode transvaser() : gardez bien en tête les 2 égalités suivantes pour vos calculs

\[volume = volume\_eau + volume\_sirop\]

\[concentration = \frac{volume\_sirop}{volume\_eau + volume\_sirop}\]

4.3 Exercice 3

    • les attributs sont privés
    • Importez la classe Point avec from point import Point

5 Template classe

class MaClasse:
    '''titre_ma_classe

    Description

    Attributes
    ----------
    attribut_1 : type_attribut_1 (str, int, float, list, bool, function...)
        desc_attribut_1
        
    attribut_2 : type_attribut_2
        desc_attribut_2.
        Valeur par défaut : False
        
    attribut_3 : type_attribut_3
        desc_attribut_3. Initialisé à 0
    '''
    
    def __init__(self, parametre_1, parametre_2):
        '''Constructeur de l'objet

        Parameters
        ----------
        parametre_1 : type_parametre_1
            desc_parametre_1
            
        parametre_2 : type_parametre_2
            desc_parametre_2
        '''
        self.attribut_1 = parametre_1
        self.attribut_2 = parametre_2
        self.attribut_3 = 0

    def ma_methode(self, param1):
        '''desc_courte_ma_methode

        desc_longue_ma_methode_facultatif       

        Parameters
        ----------
        param1 : type
            desc_param_1
            
        Returns
        -------
        type
            desc_retour

        Examples
        --------
        >>> mon_obj = ma_classe("toto")
        >>> mon_obj.ma_methode("xxx")
        2
        '''
        return 2

    def __str__(self):
        '''Conversion de l'objet en chaîne de caractères
        
        Examples
        --------
        >>> mon_obj = ma_classe("toto")
        >>> print(mon_obj)
        Classe d'attribut_1 égal à toto
        '''
        return f"Classe d'attribut_1 égal à {self.attribut_1}"

6 Template fonction

Voici un modèle que vous pouvez utiliser pour documenter vos fonctions

def ma_fonction(param1, param2):
    """description_courte

    description_longue

    Parameters
    ----------
    param1 : type (str, int, float, list...)
        description_param1
    param2 : type
        description_param2

    Returns
    -------
    type_retour
        description_retour

    Examples
    --------
    >>> 1+1
    2
    >>> print("abc")
    abc
    >>> ma_fonction(xxx, yyy)
    valeur_esperee
    """
    return None


if __name__ == '__main__':
    a = ma_fonction(p1, p2)
    print(a)

7 Template pytest

import pytest
from mon_fichier import ma_fonction

@pytest.mark.parametrize(
    'param1, param2, resultat_attendu',
    [
        (1,       1,                2),
        (1,       2,                3),
        (2,       2,                4)
    ]
)
def test_ma_fonction(param1, param2, resultat_attendu):
    assert ma_fonction(param1, param2) == resultat_attendu