Langages de script - Python

Cours 6 - classes, objets, bidules

M2 Ingénierie Multilingue - INaLCO

Clément Plancq - clement.plancq@ens.fr

Au commencement

Au commencement étaient les variables

In [ ]:
x = 27

Elles représentaient parfois des concepts sophistiqués

In [ ]:
import math

point_1 = (27, 13)
point_2 = (19, 84)

def distance(p1, p2):
    return math.sqrt((p2[0]-p1[0])**2+(p2[1]-p1[1])**2)

distance(point_1, point_2)

Et c'était pénible à écrire et à comprendre

Pour simplifier, on peut nommer les données contenues dans variables, par exemple avec un dict

In [ ]:
point_1 = {'x': 27, 'y': 13}
point_2 = {'x': 19, 'y': 84}

def distance(p1, p2):
    return math.sqrt((p2['x']-p1['x'])**2+(p2['y']-p1['y'])**2)

distance(point_1, point_2)

Et c'est toujours aussi pénible à écrire mais un peu moins à lire

On peut avoir une syntaxe plus agréable en utilisant des tuples nommés

In [ ]:
from collections import namedtuple
Point = namedtuple('Point', ('x', 'y'))

point_1 = Point(27, 13)
point_2 = Point(19, 84)

def distance(p1, p2):
    return math.sqrt((p2.x-p1.x)**2+(p2.y-p1.y)**2)

distance(point_1, point_2)

Voilà, le cours est fini, bonnes vacances.

 Peut mieux faire

  • Les trucs créés via namedtuple sont ce qu'on appelle des enregistrements (en C des structs)
  • Ils permettent de regrouper de façon lisibles des données qui vont ensemble
    • Abscisse et ordonnée d'un point
    • Année, mois et jour d'une date
    • Signifiant et signifié Prénom et nom d'une personne
  • Leur utilisation (comme tout le reste d'ailleurs) est facultative : on vit très bien en assembleur
  • Mais ils permettent de rendre le code bien plus lisible (et écrivable)
In [ ]:
Vecteur = namedtuple('Vecteur', ('x', 'y'))

v1 = Vecteur(27, 13)
v2 = Vecteur(1, 0)

def norm(v):
    return math.sqrt(v.x**2 + v.y**2)

def is_unit(v):
    return norm(v) == 1

print(is_unit(v1))
print(is_unit(v2))

C'est plutôt lisible

Mais si je veux pouvoir faire aussi de la 3d

In [ ]:
Vecteur3D = namedtuple('Vecteur3D', ('x', 'y', 'z'))

u1 = Vecteur3D(27, 13, 6)
u2 = Vecteur3D(1, 0, 0)

def norm3d(v):
    return math.sqrt(v.x**2 + v.y**2 + v.z**2)

def is_unit3d(v):
    return norm3d(v) == 1

print(is_unit3d(u1))
print(is_unit3d(u2))

C'est affreusement pénible de réécrire comme ça le même code.

Une autre solution

In [ ]:
def norm(v):
    if isinstance(v, Vecteur3D):
        return math.sqrt(v.x**2 + v.y**2 + v.z**2)
    elif isinstance(v, Vecteur):
        return math.sqrt(v.x**2 + v.y**2)
    else:
        raise ValueError('Type non supporté')

def is_unit(v):
    return norm(v) == 1

print(is_unit(v1))
print(is_unit(u2))

C'est un peu mieux mais pas top. (Même si on aurait pu trouver une solution plus intelligente)

 Ces fameux objets

Une des solutions pour faire mieux c'est de passer à la vitesse supérieure : les objets.

Ça va d'abord être un peu plus désagréable, pour ensuite être beaucoup plus agréable.

In [ ]:
class Vecteur:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def norm(self):
        return math.sqrt(self.x**2 + self.y**2)

v1 = Vecteur(27, 13)
v2 = Vecteur(1, 0)

print(v1.norm())
print(v2.norm())
In [ ]:
class Vecteur3D:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
        
    def norm(self):
        return math.sqrt(self.x**2 + self.y**2 + self.z**2)

u1 = Vecteur3D(27, 13, 6)
u2 = Vecteur3D(1, 0, 0)

print(u1.norm())
print(u2.norm())
In [ ]:
def is_unit(v):
    return v.norm() == 1

print(is_unit(v1))
print(is_unit(u2))

Le choix de la bonne fonction norme se fait automagiquement (on appelle ça du polymorphisme)

Résumons

  • Un objet, c'est un bidule qui regroupe
    • Des données (on dit attributs ou propriétés)
    • Des fonctions (on dit des méthodes)
  • Ça permet d'organiser son code de façon plus lisible et plus facilement réutilisable (croyez moi sur parole)

Et vous en avez déjà rencontré plein

In [ ]:
print(type('abc'))
print('abc'.islower())

Car en Python, tout est objet. Ce qui ne veut pas dire qu'on est obligé d'y faire attention…

POO

La programmation orientée objet (POO) est une manière de programmer différente de la programmation procédurale vue jusqu'ici.

  • Les outils de base sont les objets et les classes
  • Un concept → une classe, une réalisation concrète → un objet

C'est une façon particulière de résoudre les problèmes, on parle de paradigme, et il y en a d'autres

  • Fonctionnel : les outils de base sont les fonctions
  • Impérative : les outils de base sont les structures de contrôle (boucles, tests…)

Python fait partie des langages multi-paradigmes : on utilise le plus pratique, ce qui n'est pas sans déplaire aux puristes mais

« We are all consenting adults here »

Classes

  • On définit une classe en utilisant le mot-clé class
  • Par conventions, les noms de classe s'écrivent avec des majuscules (CapWords convention)
In [ ]:
class Word:
    """ Classe Word : définit un mot de la langue """
    pass

Pour créer un objet, on appelle simplement sa classe comme une fonction

In [ ]:
word1 = Word()
print(type(word1)) # renvoie la classe qu'instancie l'objet

On dit que word1 est une instance de la classe Word

Et il a déjà des attributs et des méthodes

In [ ]:
word1.__doc__
In [ ]:
print(dir(word1))

Et aussi un identifiant unique

In [ ]:
id(word1)
In [ ]:
word2 = Word()
id(word2)

Constructeur et attributs

  • Il existe une méthode spéciale __init__() qui automatiquement appelée lors de la création d'un objet. C'est le constructeur

  • Le constructeur permet de définir un état initial à l'objet, lui donner des attributs par exemple

  • Les attributs dans l'exemple ci-dessous sont des variables propres à un objet, une instance

In [ ]:
class Word:
    """ Classe Word : définit un mot de la langue """
    
    def __init__(self, form, lemma, pos):
        self.form = form
        self.lemma = lemma
        self.pos = pos

word = Word('été', 'être', 'V')
word.lemma
In [ ]:
word2 = Word('été', 'été', 'NOM')
word2.lemma

Méthodes

  • Les méthodes d'une classe sont des fonctions. Elles indiquent quelles actions peut mener un objet, elles peuvent donner des informations sur l'objet ou encore le modifier.
  • Par convention, on nomme self leur premier paramètre, qui fera référence à l'objet lui-même.
In [ ]:
class Word:
    """ Classe Word : définit un mot simple de la langue """
    def __init__(self, form, lemma, pos):
        self.form = form
        self.lemma = lemma
        self.pos = pos

    def is_inflected(self):
        if self.form != self.lemma:
            return True
        else:
            return False

w = Word('orientales', 'oriental', 'adj')
w.is_inflected()

Pourquoi self ? Parce que écrire w.is_inflected() c'est du sucre pour

In [ ]:
Word.is_inflected(w)

Héritage

In [ ]:
class Cake:
    """ un beau gâteau """

    def __init__(self, farine, oeuf, beurre):
        self.farine = farine
        self.oeuf = oeuf
        self.beurre = beurre

    def is_trop_gras(self):
        if self.farine + self.beurre > 500:
            return True
        else:
            return False

    def cuire(self):
        return self.beurre / self.oeuf

Cake est la classe mère.

Les classes enfants vont hériter de ses méthodes et de ses attributs.

Cela permet de factoriser le code, d'éviter les répétitions et les erreurs qui en découlent.

In [ ]:
class CarrotCake(Cake):
    """ pas seulement pour les lapins
        hérite de Cake """

    carotte = 3
    
    def cuire(self):
        return self.carotte * self.oeuf
    
class ChocolateCake(Cake):
    """ LE gâteau 
        hérite de Cake """
        
    def is_trop_gras(self):
        return False

gato_1 = CarrotCake(200, 3, 150)
gato_1.is_trop_gras()
>>> False
gato_1.cuire()
>>> 9
gato_2 = ChocolateCake(200, 6, 300)
gato_2.is_trop_gras()
>>> False