Licence CC BY-NC-ND Thierry Parmentelat

classes : rappels (1)

les classes servent à définir de nouveau types

  • en sus des types prédéfinis str, list, set, dict, …

  • plus adaptés à l’application

class

  • avec le mot-clé class on définit un nouveau type

  • une classe définit des méthodes spéciales
    ici __init__ et __repr__

class User:

    # le constructeur
    def __init__(self, name, age):
        # un objet User a deux attributs
        # name et age
        self.name = name
        self.age = age

    # l'afficheur
    def __repr__(self):
        return f"{self.name}, {self.age} ans"
# une fois qu'on a défini une classe, 
# on peut s'en servir pour créer
# des objets - on dit des instances 
# de la classe

user1 = User("Lambert", 25)
user1
Lambert, 25 ans

une classe est un type

  • comme tous les types, la classe est une usine à objets

    • user = User("Dupont", 59)

    • à rapprocher de s = set() ou n = int('32')

  • chaque objet (on dit instance) contient des données

    • rangées dans des attributs de l’objet

    • ici name et age

affichage

  • la méthode spéciale __repr__(self) doit renvoyer une chaine

  • elle est utilisée pour

    • imprimer l’objet avec print()

    • convertir un objet en chaine

print(f"je viens de voir {user1}")
je viens de voir Lambert, 25 ans
str(user1)
'Lambert, 25 ans'

affichage et conversion en chaine

en fait il est possible d’être plus fin, et de définir deux méthodes spéciales qui sont

  • __repr__(self) et

  • __str__(self)

cela dit pour commencer on peut se contenter de ne définir que __repr__() qui est alors utilisée pour tous les usages

méthodes

  • une classe peut définir des méthodes

  • qui sont des fonctions qui s’appliquent sur un objet (de cette classe)

# une implémentation très simple
# d'une file FILO
# premier entré dernier sorti

class Stack:
    
    def __init__(self):
        self.frames = [] 
        
    def __repr__(self):
        return " > ".join(self.frames)            
    
    def push(self, item):
        self.frames.append(item)
        
    def pop(self):
        return self.frames.pop()
# instance
stack = Stack()

stack.push('fact(3)')
stack.push('fact(2)')
stack.push('fact(1)')

stack
fact(3) > fact(2) > fact(1)
stack.pop()
'fact(1)'
stack
fact(3) > fact(2)

méthodes et paramètres

remarquez qu’ici

  • on a défini la méthode push avec 2 paramètres

def push(self, item):
  • ce qui fait qu’on peut l’appeler sur un objet avec 1 paramètre

stack.push(some_item)
  • car le premier paramètre self est lié
    à l’objet sur lequel on envoie la méthode

  • et la phrase stack.push(some_item)
    est en fait équivalente à Stack.push(stack, some_item)

intérêts de cette approche

  • définir vos propres types de données

  • grouper les données qui vont ensemble dans un
    objet unique, facile à passer à d’autres fonctions

  • invariants: garantir de bonnes propriétés si on utilise les objets au travers des méthodes

et aussi (sera vu ultérieurement)

  • héritage

    • réutiliser une classe en modifiant
      seulement quelques aspects

  • intégrer les objets dans le langage
    i.e. donner un sens à des constructions comme

    • x in obj

    • obj[x]

    • if obj:

    • for item in obj:

exemples

exemple : np.ndarray

  • c’est une classe que vous utilisez tous les jours !

  • en fait il n’y a pas de différence de fond

    • entre les types prédéfinis (str, …)

    • et les classes créées avec class

exemple : class Point

import math

class Point:

    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f"({self.x:.2f} x {self.y:.2f})"
    
    def distance(self, other):
        return math.sqrt((self.x-other.x)**2 + (self.y-other.y)**2)
a = Point(4, 3)
b = Point(7, 7)
a, b
((4.00 x 3.00), (7.00 x 7.00))
a.distance(b)
5.0

exemple : class Circle (1)

class Circle1:

    def __init__(self, center: Point, radius: float):
        self.center = center
        self.radius = radius
        
    def __repr__(self):
        return f"[{self.center}{self.radius:.2f}]"
    
    def contains(self, point: Point):
        """
        returns a bool; does point belong in the circle ?
        """
        print(self.center.distance(point))
        return math.isclose(self.center.distance(point), self.radius)
c1 = Circle1(Point(0, 0), 5)
c1.contains(a)
5.0
True

exemple : class Circle (2)

class Circle2:

    def __init__(self, center: Point, radius: float):
        self.center = center
        self.radius = radius
        
    def __repr__(self):
        return f"[{self.center}{self.radius:.2f}]"
    
    # si on transforme cette méthode en méthode spéciale...
    def __contains__(self, point: Point):
        """
        returns a bool; does point belong in the circle ?
        """
        print(self.center.distance(point))
        return math.isclose(self.center.distance(point), self.radius)
c2 = Circle2(Point(0, 0), 5)

# alors on peut faire le même calcul, mais
# l'écrire comme un test d'appartenance habituel 'x in y'
a in c2
5.0
True

exemple : class datetime.date etc..

  • bien sûr il y a des classes dans la bibliothèque standard

  • voyez par exemple le module datetime

  • et notamment datetime.date (une date)
    et datetime.timedelta (une durée)

  • bien sûr il y a des classes dans la bibliothèque standard

  • voyez par exemple le module datetime

  • et notamment datetime.date (une date)
    et datetime.timedelta (une durée)

# normalement la classe date aurait dû s'appeler Date
from datetime import date as Date
# pareil
from datetime import timedelta as TimeDelta

Date.today()
datetime.date(2021, 8, 16)
TimeDelta(weeks=2)
datetime.timedelta(days=14)
# il y a 3 semaines nous étions le
today = Date.today()
three_weeks = 3 * TimeDelta(weeks=1)

today - three_weeks
datetime.date(2021, 7, 26)
def timedelta_as_year_month(age):
    """
    convert a duration in years and months (as a str)
    """
    year = TimeDelta(days=365.2425)
    years, leftover = age // year, age % year
    month = year/12
    months, leftover = leftover // month, leftover % month
    return f"{years} ans, {months} mois"

exemple : class Student

class Student:
    
    def __init__(self, first_name, last_name, 
                 birth_year, birth_month, birth_day):
        self.first_name = first_name
        self.last_name = last_name
        self.birth_year = birth_year
        self.birth_month = birth_month
        self.birth_day = birth_day
        
    def __repr__(self):
        return f"{self.first_name} {self.last_name}"
    
    def age(self):
        """
        retourne un TimeDelta
        """
        birth = Date(self.birth_year, self.birth_month, self.birth_day)
        now = Date.today()
        # la différence entre 2 Dates c'est une 
        return now - birth
    
    def repr_age(self):
        return timedelta_as_year_month(self.age())

class Student - utilisation

achille = Student("Achille", "Talon", 2001, 7, 14)
achille
Achille Talon
achille.age()
datetime.timedelta(days=7338)
type(achille.age())
datetime.timedelta
print(f"{achille} a {achille.repr_age()}")
Achille Talon a 20 ans, 1 mois

exemple : class Class

(dans le sens: groupe de Students)

  • bien sûr on peut combiner nos types (les classes)
    avec les types de base

  • et ainsi créer e.g. des listes de Student

class Class:
    
    def __init__(self, classname, students):
        self.classname = classname
        self.students = students
        
    def __repr__(self):
        return f"{self.classname} with {len(self.students)} students"
        
    def average_age(self):
        # on aimerait pouvoir écrire simplement ceci
        # return ((sum(student.age() for student in self.students)
        #          / len(self.students))
        # mais ça ne fonctionne pas, il faut passer à sum 
        # l'élément neutre de l'addition - ici TimeDelta(0)
        # car '0' ne peut pas s'additionner à un TimeDelta
        return (sum((student.age() for student in self.students), TimeDelta(0)) 
                / len(self.students))

class Class - utilisation

hilarion = Student("Hilarion", "Lefuneste", 1998, 10, 15)
gaston = Student("Gaston", "Lagaffe", 1995, 2, 28)
haddock = Student("Capitaine", "Haddock", 2000, 1, 14)
tournesol = Student("Professeur", "Tournesol", 1996, 2, 29)

# attention je ne peux pas utiliser une variable 
# qui s'appellerait 'class' car c'est un mot-clé de Python

cls = Class("CIC1A", [achille, hilarion, gaston, haddock, tournesol])
cls
CIC1A with 5 students
# la moyenne d'âge de la classe
cls.average_age()
datetime.timedelta(days=8506)
# la moyenne d'âge de la classe, pour les humains
timedelta_as_year_month(cls.average_age())
'23 ans, 3 mois'

résumé (1/2)

  • avec class on peut définir un nouveau type

    • qui nous permet de créer des objets

    • qui représentent mieux que les types de base les données de notre application

  • pas de différence entre un type prédéfini et une classe :
    un objet créé par une classe s’utilise normalement

    • une variable peut désigner un objet

    • un objet peut être dans une liste (ou autre type) builtin
      (attention pour les clés de dict qui doivent être immutables)

    • ou passé en paramètre à une fonction,

    • etc, etc…

résumé (2/2)

  • une classe peut définir des méthodes

    • qui travaillent sur un objet (souvent appelé self)

    • souvent on ne modifie les objets
      qu’au travers des méthodes fournies par la classe

    • ce qui permet de garantir certains invariants