Skip to article frontmatterSkip to article content

les classes servent à définir de nouveau types

class

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

affichage

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

méthodes

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

class Stack:

    ## méthodes spéciales
    def __init__(self):
        self._frames = [] 
        
    def __repr__(self):
        return " > ".join(self._frames)            

    ## méthodes (usuelles)
    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

intérêts de cette approche

méthodes et encapsulation

avec la Stack, on est censé utiliser seulement stack.push() et stack.pop()
et pas directement stack._frames (d’où le _ au début de l’attribut)

cette technique permet de séparer :

de façon à pouvoir changer l’implémentation sans changer l’interface
et ainsi e.g. améliorer le comportement sans changer le code utilisateur

exemples de classes

exemple : np.ndarray et pd.DataFrame

import pandas as pd

# on crée typiquement une dataframe à partir d'un csv
df = pd.read_csv("../data/Worldwide-Earthquake-database.csv")

# et on obtient .. un objet de la classe pd.DataFrame
# (c'est plus pratique que une liste de dictionnaires ou autres)
# et sur lequel on a des méthodes comme ici .head()

df.head(4)
Loading...
# mais aussi on peut faire plein d'autres choses 
# avec les opérateurs Python, comme faire des recherches:

df[df.TOTAL_INJURIES > 300_000]
Loading...

exemple : class Point

un grand classique: on groupe les coordonnées x et y dans un objet

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)
# on crée deux instances

a = Point(4, 3)
b = Point(7, 7)
a, b
((4.00 x 3.00), (7.00 x 7.00))
# on appelle la méthode distance

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"[Circle: {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
[Circle: (0.00 x 0.00) ⟷ 5.00]
c1.contains(a)
5.0
True

exemple : class Circle (2)

la même chose exactement, mais en utilisant une méthode spéciale

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
# techniquement, on a aussi le droit d'écrire ceci
# mais IL NE FAUT PAS LE FAIRE ce n'est pas du tout l'esprit..

c2.__contains__(a)
5.0
True

exemple : class datetime.date etc..

# normalement si on avait appliqué la PEP008 à l'époque,
# la classe date aurait dû s'appeler Date
from datetime import date as Date

# et pareil
from datetime import timedelta as TimeDelta

Date.today()
datetime.date(2025, 10, 16)
# ici je crée un objet 'durée' 

TimeDelta(weeks=2)
datetime.timedelta(days=14)
# et je peux faire de l'arithmétique; par exemple:

today = Date.today()

# multiplier une durée
three_weeks = 3 * TimeDelta(weeks=1)

# ajouter ou retrancher une durée à une date
# il y a 3 semaines nous étions le
today - three_weeks
datetime.date(2025, 9, 25)
# pour afficher une durée avec un format qui nous convient
# on s'en servira plus tard...

def timedelta_as_year_month(age: TimeDelta) -> str:
    """
    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

voyons maintenant une classe plus orientée “gestion”, i.e. juste une collection de données

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_date = Date(birth_year, birth_month, birth_day)
        
    def __repr__(self):
        return f"{self.first_name} {self.last_name}"
    
    def age(self) -> TimeDelta:
        # the difference between 2 Dates is a TimeDelta
        return Date.today() - self.birth_date
    
    def repr_age(self) -> str:
        return timedelta_as_year_month(self.age())

class Student - utilisation

# création d'une instance

achille = Student("Achille", "Talon", 2001, 7, 14)

# affichage
achille
Achille Talon
# une méthode ordinaire

achille.age()
datetime.timedelta(days=8860)
# si on voulait une présentation plus ad hoc

print(f"{achille} a {achille.repr_age()}")
Achille Talon a 24 ans, 3 mois

exemple : class Class

dans le sens: groupe de Students - rien à voir avec le mot-clé class !

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):
        return (sum((student.age() for student in self.students), TimeDelta(0)) 
                / len(self.students))

class Class - utilisation

on peut alors utiliser cette nouvelle classe comme ceci

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=10028)
# la moyenne d'âge de la classe, pour les humains
timedelta_as_year_month(cls.average_age())
'27 ans, 5 mois'

héritage

voici un dernier exemple, où on utilise cette fois massivement l’héritage
en effet ce programme s’appuie sur la lib arcade (on fait comment pour l’installer déjà ?) qui expose une API très fortement influencée par l’héritage entre classes
en principe il est complet (à copier-coller dans un fichier) et il affiche un boid (avec une petit flêche) qui avance tout seul

bon en soi ça n’est pas très impressionnant; mais regardez bien c’est intéressant quand même !

import math

# arcade offers an OO API based on inheritance
import arcade

# the image for the boid
IMAGE = "../media/arrow-resized.png"

WIDTH, HEIGHT = 200, 200

# this is inheritance
class Boid(arcade.Sprite):

    def __init__(self):
        super().__init__(IMAGE)
        self.center_x, self.center_y = 100, 100
        self.angle = 30

    def on_update(self, delta_time):
        self.center_x += 100 * delta_time * math.cos(math.radians(self.angle))
        self.center_y += 100 * delta_time * math.sin(math.radians(self.angle))

        self.center_x %= WIDTH
        self.center_y %= HEIGHT


# and again
class Window(arcade.Window):

    def __init__(self):
        super().__init__(WIDTH, HEIGHT, "My first boid")
        self.boid = Boid()

    def on_draw(self):
        arcade.start_render()
        self.boid.draw()

    def on_update(self, delta_time):
        self.boid.on_update(delta_time)

window = Window()

# observe that we don't need to write the mainloop !
arcade.run()

résumé (1/2)

résumé (2/2)