
classes¶
types prédéfinis et monde réel¶
les types prédéfinis que l’on a vus jusqu’ici
i.e. nombres, chaines, containers
(pour rappel: bool
, int
, float
, str
, list
, set
, dict
)
sont pratiques et puissants
MAIS souvent cela n’est pas suffisant pour traiter des problèmes réels
typiquement¶
on a très souvent besoin de manipuler des données ‘composites’
e.g. un ensemble de personnes, de villes, de compagnies aériennes
en termes de modèle de données, ce sont des ‘enregistrements’
c’est-à-dire en fait des données composites
# une façon naive d'implémenter une donnée composite
# est d'utiliser un dictionnaire
personne = {'name': 'Dupont', 'age': 32}
utiliser un dictionnaire peut faire l’affaire
pour des applications simples
(c’est par exemple ce qu’on récupère d’un fichier JSON)
mais c’est vite un peu lourd et très limité
class
¶
dans ces cas-là (et dans bien d’autres)
il est plus flexible de se définir un nouveau type
qui permette de créer des objets qui ont les propriétés en question
que dans ce contexte on appelle des attributs
# voici comment définir
# une classe `User`
class User:
def __init__(self, name, age):
self.name = name
self.age = age
# 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)
instances¶
remarquez qu’ici
on a défini la méthode spéciale
__init__
avec 3 paramètres
def __init__(self, name, age)
ce qui fait qu’on peut appeler (une fonction du nom de) la classe avec 2 paramètres
User("Lambert", 25)`
car le premier paramètre est l’objet en cours de création !
et la phrase
self.name = name
signifie que l’on attache le paramètrename
dans l’attributname
de l’objet en cours de création
affichage¶
# si on ne fait rien de spécial,
# l'affichage est vraiment vilain
user1
# qui vous dit que user1 est un objet dans le module __main__
# qu'il est de de type User
# et où il est dans la mémoire de votre ordi
# mais ce n'est sûrement pas ce que vous voulez voir !
<__main__.User at 0x7fc9dcfe8d00>
# pour améliorer cela:
# vous définissez la représentation de vos objets de type User
class User:
def __init__(self, name, age):
self.name = name
self.age = age
# définit comment afficher
# les objets de cette classe
def __repr__(self):
return f"{self.name}, {self.age} ans"
user2 = User("Martin", 22)
user2
Martin, 22 ans
méthodes¶
comme on l’a évoqué dans la section “objets, types et méthodes”
dans une classe on 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 push(self, item):
self.frames.append(item)
def pop(self):
return self.frames.pop()
def __repr__(self):
return " > ".join(self.frames)
# 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 - suite¶
à nouveau, remarquez que la définition d’une méthode
prévoit un paramètre de plus que l’appel de la méthode
car obj.methode(...)
est équivalent à
methode(obj, ...)
intérêts de cette approche¶
définir vos propres types de données
grouper les données qui vont ensemble dans un objet
facile à passer à d’autres fonctionsinvariants: garantir de bonnes propriétés
si on utilise les objets au travers des méthodesorganise les espaces de noms
e.g. pas de conflit entre Class1.name et Class2.namehéritage - ne sera pas détaillé dans ce cours
objets et langage¶
le langage “connaît” bien sûr les types prédéfinis
c’-à-d qu’il sait “faire la bonne chose”
selon le type des objets qu’il manipule
exemples, selon le type de obj
:
print(obj)
opérateurs comme
obj + x
itération
for item in obj
len(obj)
test d’appartenance
x in obj
indexation
obj[x]
etc..
exemple - if obj:
¶
remarquez qu’on peut toujours écrire un test if
(ou while
)
même si le sujet du test n’est pas un booléen
if 0:
print('bingo')
if 2:
print('bingo')
bingo
if []:
print('bingo')
if [1]:
print('bingo')
bingo
la règle pour les types prédéfinis est que dans un test
0
, 0.0
, ""
, []
, set()
, {}
sont considérés comme False
les autres valeurs sont considérées comme True
méthodes spéciales¶
les méthodes spéciales sont toutes de la forme __bidule__
on en a déjà vu 2 : __init__
et __repr__
le langage en prévoit plusieurs dizaines (optionnelles)
exemple de méthode spéciale¶
exemple
lorsqu’un objet est utilisé dans un if obj:
il est d’abord converti en booléen par bool(obj)
pour redéfinir ce que cela veut dire sur une classe
on peut définir la méthode spéciale __bool__
# une classe jouet
# uniquement à but pédagogique ...
class Fool:
"""
les objets de cette classe
se comportent à l'envers
par rapport aux tests
if ou while
"""
def __init__(self, value):
self.value = value
# on triche, le test
# va marcher à l'envers !
def __bool__(self):
return not bool(self.value)
# un test sur un objet
# de cette classe
fool_true = Fool(True)
# a un comportement inattendu
if fool_true:
print('bingo')
if Fool([]):
print('bingo')
bingo
résumé¶
on définit une classe avec le mot clé
class
avec les classes on peut étendre les types prédéfinis
int
,str
,list
, …un objet dans une classe contient typiquement des attributs
une classe définit typiquement des méthodes
on accède aux attributs et méthodes avec la syntaxe
object.attribut
ouobject.methode()
grâce aux méthodes spéciales, on peut bien intégrer une classe dans le langage
la classe Quaternion
¶
En option
on peut aussi redéfinir les opérateurs arithmétiques
comme +
et *
avec les
méthodes spéciales __add__
et __mul__
Application: une micro-classe qui implémente les quaternions
https://fr.wikipedia.org/wiki/Quaternion
le corps non commutatif engendré sur $\mathbb{R}$ par trois éléments $i, j, k$ tels que
$$i^2 = j^2 = k^2 = ijk = -1$$
un quaternion s’écrit donc
$q = a + bi + cj + dk$ avec $(a, b, c, d) \in \mathbb{R}^4$
class Quaternion:
def __init__(self, a, b, c, d):
self.implem = (a, b, c, d)
# c'est la partie intéressante
def __add__(self, other):
"""defines q1 + q2)"""
return Quaternion(*(x+y for x, y in zip(self.implem, other.implem)))
def __mul__(self, other):
"""defines q1 * q2"""
a1, b1, c1, d1 = self.implem
a2, b2, c2, d2 = other.implem
a = a1 * a2 - b1 * b2 - c1 * c2 - d1 * d2
b = a1 * b2 + b1 * a2 + c1 * d2 - d1 * c2
c = a1 * c2 + c1 * a2 + d1 * b2 - b1 * d2
d = a1 * d2 + d1 * a2 + b1 * c2 - c1 * b2
return Quaternion(a, b, c, d)
def __eq__(self, other):
"""implements q1 == q2"""
return self.implem == other.implem
def __repr__(self):
a, b, c, d = self.implem
return f"({a}, {b}, {c}, {d})"
q0 = Quaternion(0, 0, 0, 0)
q1 = Quaternion(1, 0, 0, 0)
q_1 = Quaternion(-1, 0, 0, 0); q_1
(-1, 0, 0, 0)
i = Quaternion(0, 1, 0, 0)
j = Quaternion(0, 0, 1, 0)
k = Quaternion(0, 0, 0, 1)
k
(0, 0, 0, 1)
i * i
(-1, 0, 0, 0)
i*i == j*j == k*k == i*j*k == q_1
True
q = Quaternion(1, 2, 3, 4)
q * q
(-28, 4, 6, 8)
q * i
(-2, 1, 4, -3)
i * q
(-2, 1, -4, 3)
limitations pour cette version rustique :
manque des opérations
égalité sans doute trop stricte (arrondis)
ne sait pas interagir avec
int
oufloat
affichage
…
# pour affichage
labels = ['', 'i', 'j', 'k']
# un code possible pour un affichage plus élégant
# affichage
def __repr__(self):
# on prépare des morceaux comme '3', '2i', '4j', '5k'
# mais seulement si la dimension en question n'est pas nulle
parts = (f"{x:.1f}{label}" for x, label in zip(self.implem, labels) if x)
# on les assemble avec un + au milieu
full = " + ".join(parts)
# si c'est vide c'est que self est nul
return full if full != "" else "0"
# ce qui donnerait aussi en version un peu plus condensée
# mais beaucoup moins lisible
# return (" + ".join(f"{x:.2f}{label}" for x, label in zip(self.implem, labels) if x) or "0")