IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Pensez en Python

Comment maîtriser la science de l'informatique


précédentsommairesuivant

17. Classes et méthodes

Bien que nous y ayons utilisé certaines fonctionnalités orientées objet de Python, les programmes des deux derniers chapitres ne sont pas vraiment orientés objet parce qu'ils ne représentent pas les relations entre les types définis par le programmeur et les fonctions qui opèrent sur eux. La prochaine étape est de transformer ces fonctions en des méthodes qui rendent les relations explicites.

Les exemples de code de ce chapitre sont disponibles sur Time2.py, et les solutions aux exercices se trouvent à l'adresse Point2_soln.py.

17-1. Fonctionnalités orientées objet

Python est un langage de programmation orienté objet, ce qui signifie qu'il offre des fonctionnalités qui prennent en charge la programmation orientée objet, laquelle présente ces caractéristiques déterminantes :

  • les programmes incluent des définitions de classes et de méthodes ;
  • la plus grande partie du calcul est exprimé en tant qu'opérations sur des objets ;
  • les objets représentent souvent des choses dans le monde réel, et les méthodes correspondent souvent à la manière dont les choses du monde réel interagissent.

Par exemple, la classe Temps définie au chapitre 16Classes et fonctions correspond à la façon dont on enregistre habituellement le moment de la journée, et les fonctions que nous avons définies correspondent aux types de choses que l'on fait avec des temps. De même, les classes Point et Rectangle du chapitre 15Classes et objets correspondent aux concepts mathématiques de point et de rectangle.

Jusqu'à présent, nous n'avons pas profité des fonctionnalités que Python fournit pour soutenir la programmation orientée objet. Ces fonctionnalités ne sont pas strictement nécessaires ; la plupart d'entre elles fournissent une syntaxe de rechange pour les choses que nous avons déjà faites. Mais dans de nombreux cas, cette nouvelle syntaxe est plus concise et transmet la structure du programme de façon plus précise.

Par exemple, dans Time1.py, il n'y a aucun lien évident entre la définition de la classe et les définitions de fonctions qui suivent. Après un bref examen, il est évident que chaque fonction prend comme argument au moins un objet Temps.

Cette observation est la motivation pour les méthodes ; une méthode est une fonction associée à une classe particulière. Nous avons vu des méthodes relatives aux chaînes de caractères, listes, dictionnaires et tuples. Dans ce chapitre, nous allons définir des méthodes pour les types définis par le programmeur.

Les méthodes sont sémantiquement la même chose que des fonctions, mais il y a deux différences syntaxiques :

  • les méthodes sont définies à l'intérieur d'une définition de classe afin de rendre explicite la relation entre la classe et ses méthodes ;
  • la syntaxe d'invocation d'une méthode est différente de la syntaxe d'appel d'une fonction.

Dans les prochaines sections, nous allons prendre les fonctions des deux chapitres précédents et les transformer en méthodes. Cette transformation est purement mécanique ; vous pouvez le faire en suivant une séquence d'étapes. Si vous êtes à l'aise avec la conversion d'une forme à une autre, vous serez en mesure de choisir la meilleure forme pour tout ce que vous faites.

17-2. Afficher des objets

Dans le chapitre 16Classes et fonctions, nous avons défini une classe nommée Temps et dans la section 16.1Temps, vous avez écrit une fonction nommée afficher_temps :

 
Sélectionnez
class Temps:
    """Représente le moment de la journée."""

def afficher_temps(temps):
    print('%.2d:%.2d:%.2d' % (temps.heure, temps.minute, temps.seconde))

Pour appeler cette fonction, vous devez passer comme argument un objet Temps.

 
Sélectionnez
>>> debut = Temps()
>>> debut.heure = 9
>>> debut.minute = 45
>>> debut.seconde = 00
>>> afficher_temps(debut)
09:45:00

Pour faire de la fonction afficher_temps une méthode, tout ce que nous devons faire est de déplacer la définition de la fonction à l'intérieur de la définition de classe.

 
Sélectionnez
class Temps:
    def afficher_temps(temps):
        print('%.2d:%.2d:%.2d' % (temps.heure, temps.minute, temps.seconde))

Maintenant, il y a deux façons d'appeler afficher_temps. La première (et la moins courante) est d'utiliser la syntaxe de fonction :

 
Sélectionnez
>>> Temps.afficher_temps(debut)
09:45:00

Dans cette utilisation de la notation pointée, Temps est le nom de la classe et afficher_temps est le nom de la méthode. debut est passé en paramètre.

La seconde façon, plus concise, est d'utiliser la syntaxe de méthode :

 
Sélectionnez
>>> debut.afficher_temps()
09:45:00

Dans cette utilisation de la notation pointée, afficher_temps est (toujours) le nom de la méthode et debut est l'objet sur lequel la méthode est invoquée, qui s'appelle le sujet. Tout comme le sujet d'une phrase est ce dont parle la phrase, le sujet d'un appel de méthode est ce à quoi s'applique la méthode.

À l'intérieur de la méthode, le sujet est affecté au premier paramètre, donc dans ce cas debut est affecté à temps.

Par convention, le premier paramètre d'une méthode s'appelle self, donc il serait plus usuel d'écrire afficher_temps comme ceci :

 
Sélectionnez
class Temps:
    def afficher_temps(self):
        print('%.2d:%.2d:%.2d' % (self.heure, self.minute, self.seconde))

La raison de cette convention est une métaphore implicite :

  • la syntaxe d'un appel de fonction, afficher_temps(debut), suggère que la fonction est l'agent actif. Il dit quelque chose comme « Hé, afficher_temps ! Voici un objet à afficher » ;
  • dans la programmation orientée objet, les objets sont les agents actifs. Un appel de méthode comme debut.afficher_temps() dit « Hé, debut ! S'il te plaît, affiche toi-même ».

Ce changement de perspective est peut-être plus poli, mais il n'est pas évident que cela soit très utile. Dans les exemples que nous avons vus jusqu'à présent, cela pourrait ne pas l'être. Mais parfois, le déplacement de la responsabilité des fonctions sur les objets rend possible l'écriture des fonctions (ou méthodes) plus polyvalentes, et rend le code plus facile à maintenir et à réutiliser.

À titre d'exercice, réécrivez temps_vers_int (de la section 16.4Prototypage versus planification) comme une méthode. Vous pourriez être tenté à réécrire également int_vers_temps comme une méthode, mais cela n'a pas vraiment de sens, car il n'y a aucun objet sur lequel elle serait invoquée.

17-3. Un autre exemple

Voici une version de la fonction incremente (de la section 16.3Modificateurs) réécrite comme une méthode :

 
Sélectionnez
# à l'intérieur de la classe Temps :

    def incremente(self, secondes):
        secondes += self.temps_vers_int()
        return int_vers_temps(secondes)

Cette version suppose que temps_vers_int soit écrite comme une méthode. En outre, notez que c'est une fonction pure, pas un modificateur.

Voici comment vous invoquez incremente :

 
Sélectionnez
>>> debut.afficher_temps()
09:45:00
>>> fin = debut.incremente(1337)
>>> fin.afficher_temps()
10:07:17

Le sujet, debut, est attribué au premier paramètre, self. L'argument, 1337, est attribué au second paramètre, secondes.

Ce mécanisme peut être source de confusion, surtout si vous faites une erreur. Par exemple, si vous invoquez incremente avec deux arguments, vous obtenez :

 
Sélectionnez
>>> end = start.incremente(1337, 460)
TypeError: incremente() takes 2 positional arguments but 3 were given

Le message d'erreur est au départ déroutant, car il y a seulement deux arguments entre les parenthèses. Mais le sujet est également considéré comme un argument, si bien qu'il y en a trois en tout.

Au fait, un argument positionnel est un argument qui ne possède aucun nom de paramètre ; autrement dit, ce n'est pas un argument mot-clé. Dans cet appel de fonction :

 
Sélectionnez
sketch(perroquet, cage, mort=True)

perroquet et cage sont des arguments positionnels et mort est un argument mot-clé.

17-4. Un exemple plus compliqué

La réécriture de est_apres (de la section 16.1Temps) est un peu plus compliquée, car elle prend en paramètres deux objets Temps. Dans ce cas, la convention est de nommer le premier paramètre self et le second paramètre other :

 
Sélectionnez
# à l'intérieur de la classe Temps:

    def est_apres(self, other):
        return self.temps_vers_int() > other.temps_vers_int()

Pour utiliser cette méthode, vous devez l'invoquer sur un objet et passer l'autre comme argument :

 
Sélectionnez
>>> fin.est_apres(debut)
True

Une bonne chose à propos de cette syntaxe est qu'elle se lit presque comme en français : « fin est(-elle) après début ? »

17-5. La méthode init

La méthode init (raccourci de « initialisation ») est une méthode spéciale qui est appelée automatiquement lorsqu'un objet est instancié. Son nom complet est __init__ (deux caractères de soulignement, suivis par init, puis deux autres caractères de soulignement). Une méthode init pour la classe Temps pourrait ressembler à ceci :

 
Sélectionnez
# à l'intérieur de la classe Temps :

    def __init__(self, heure = 0, minute = 0, seconde = 0):
        self.heure = heure
        self.minute = minute
        self.seconde = seconde

Il est courant que les paramètres de __init__ aient les mêmes noms que les attributs. L'instruction

 
Sélectionnez
self.heure = heure

stocke la valeur du paramètre heure en tant qu'attribut de self.

Les paramètres sont facultatifs, donc si vous appelez Temps sans arguments, vous obtenez les valeurs par défaut.

 
Sélectionnez
>>> temps = Temps()
>>> temps.afficher_temps()
00:00:00

Si vous passez un argument, il remplace heure :

 
Sélectionnez
>>> temps = Temps(9)
>>> temps.afficher_temps()
09:00:00

Si vous fournissez deux arguments, ils remplacent heure et minute.

 
Sélectionnez
>>> temps = Temps(9, 45)
>>> temps.afficher_temps()
09:45:00

Et si vous fournissez trois arguments, ils remplacent les trois valeurs par défaut.

À titre d'exercice, écrivez une méthode init pour la classe Point, qui prend x et y comme paramètres optionnels et les assigne aux attributs correspondants.

17-6. La méthode __str__

__str__ est une méthode spéciale, comme __init__, qui est censée renvoyer une représentation sous forme de chaîne de caractères d'un objet.

Par exemple, voici une méthode str pour les objets Temps :

 
Sélectionnez
# à l'intérieur de la classe Temps :

    def __str__(self):
        return '%.2d:%.2d:%.2d' % (self.heure, self.minute, self.seconde)

Lorsque vous affichez un objet, Python invoque la méthode str :

 
Sélectionnez
>>> temps = Temps(9, 45)
>>> print(temps)
09:45:00

Quand j'écris une nouvelle classe, je commence presque toujours par écrire __init__, qui rend plus facile l'instanciation des objets, et __str__, qui est utile pour le débogage.

À titre d'exercice, écrivez une méthode str pour la classe Point. Créez un objet Point et affichez-le.

17-7. Surcharge d'opérateur

En définissant d'autres méthodes spéciales, vous pouvez spécifier le comportement des opérateurs sur les types définis par le programmeur. Par exemple, si vous définissez une méthode nommée __add__ pour la classe Temps, vous pouvez utiliser l'opérateur + sur les objets de type Temps.

Voici à quoi pourrait ressembler la définition :

 
Sélectionnez
# à l'intérieur de la classe Temps :

    def __add__(self, other):
        secondes = self.temps_vers_int() + other.temps_vers_int()
        return int_vers_temps(seconds)

Et voici comment vous pouvez l'utiliser :

 
Sélectionnez
>>> debut = Temps(9, 45)
>>> duree = Temps(1, 35)
>>> print(debut + duree)
11:20:00

Lorsque vous appliquez l'opérateur + sur des objets de type Temps, Python invoque automatiquement __add__. Lorsque vous affichez le résultat, Python invoque __str__. Donc, il y a beaucoup de choses qui se passent dans les coulisses !

La modification du comportement d'un opérateur afin qu'il fonctionne avec des types définis par le programmeur s'appelle la surcharge d'opérateur. Pour chaque opérateur de Python, il existe une méthode spéciale correspondante, comme __add__. Pour plus de détails, voir http://docs.python.org/3/reference/datamodel.html#specialnames.

À titre d'exercice, écrivez une méthode add pour la classe Point.

17-8. Résolution de méthode basée sur le type

Dans la section précédente, nous avons additionné deux objets Temps, mais vous pouvez également additionner un nombre entier à un objet Temps. Ce qui suit est une version de __add__ qui vérifie le type de other et invoque soit ajouter_temps, soit incremente :

 
Sélectionnez
# à l'intérieur de la classe Temps :

    def __add__(self, other):
        if isinstance(other, Temps):
            return self.ajouter_temps(other)
        else:
            return self.incremente(other)

    def ajouter_temps(self, other):
        secondes = self.temps_vers_int() + other.temps_vers_int()
        return int_vers_temps(secondes)

    def incremente(self, secondes):
        secondes += self.temps_vers_int()
        return int_vers_temps(secondes)

La fonction interne isinstance prend une valeur et un objet classe, et retourne True si la valeur est une instance de la classe.

Si other est un objet de type Temps, __add__ invoque ajouter_temps. Sinon, il suppose que le paramètre est un nombre et invoque incremente. Cette opération s'appelle résolution de méthode basée sur le type, car elle choisit la méthode à employer sur la base du type des arguments.

Voici des exemples qui utilisent l'opérateur + avec différents types :

 
Sélectionnez
>>> debut = Time(9, 45)
>>> duree = Time(1, 35)
>>> print(debut + duree)
11:20:00
>>> print(debut + 1337)
10:07:17

Malheureusement, cette mise en œuvre de l'addition n'est pas commutative. Si le nombre entier est le premier opérande, vous obtenez

 
Sélectionnez
>>> print(1337 + debut)
TypeError: unsupported operand type(s) for +: 'int' and 'instance'

Le problème est qu'au lieu de demander à l'objet de type Temps d'additionner un nombre entier, Python demande au nombre entier d'additionner un objet de type Temps, et il ne sait pas comment le faire. Mais il existe une solution intelligente pour ce problème : la méthode spéciale __radd__, qui signifie right-side add, « additionner à droite ». Cette méthode est invoquée quand un objet de type Temps apparaît du côté droit de l'opérateur +. Voici la définition :

 
Sélectionnez
# à l'intérieur de la classe Temps :

    def __radd__(self, other):
        return self.__add__(other)

Et voici comment l'utiliser :

 
Sélectionnez
>>> print(1337 + debut)
10:07:17

À titre d'exercice, écrivez une méthode add pour des objets Point qui fonctionne sur un objet de type Point ou un tuple :

  • si le second opérande est un Point, la méthode doit retourner un nouveau Point dont l'abscisse x est la somme des abscisses x des opérandes, et dont l'ordonnée y est la somme des ordonnées y ;
  • si le second opérande est un tuple, la méthode doit additionner le premier élément du tuple à l'abscisse x et le second élément à l'ordonnée y, et renvoyer un nouveau Point dont les coordonnées représentent le résultat.

17-9. Polymorphisme

La résolution de méthode basée sur le type est utile quand elle est nécessaire, mais elle n'est (heureusement) pas toujours nécessaire. Souvent, vous pouvez l'éviter en écrivant des fonctions qui s'exécutent correctement pour des arguments de types différents.

La plupart des fonctions que nous avons écrites pour les chaînes de caractères acceptent aussi d'autres types de séquences. Par exemple, dans la section 11.2Un dictionnaire comme une collection de compteurs, nous utilisions un histogramme pour compter le nombre de fois où chaque lettre apparaît dans un mot.

 
Sélectionnez
def histogramme(s):
    d = dict()
    for c in s:
        if c not in d:
            d[c] = 1
        else:
            d[c] = d[c]+1
    return d

Cette fonction peut être utilisée également avec des listes, tuples, et même des dictionnaires, à condition que les éléments de s soient hachables, de sorte qu'ils puissent être utilisés comme clés dans d.

 
Sélectionnez
>>> t = ['spam', 'omelette', 'spam', 'spam', 'bacon', 'spam']
>>> histogramme(t)
{'bacon': 1, 'omelette': 1, 'spam': 4}

Les fonctions qui acceptent plusieurs types sont dites polymorphes. Le polymorphisme peut faciliter la réutilisation du code. Par exemple, la fonction interne sum, qui ajoute des éléments à une séquence, fonctionne tant que les éléments de la séquence supportent l'addition.

Comme les objets de type Temps fournissent une méthode add, ils peuvent être passés en argument à sum :

 
Sélectionnez
>>> t1 = Temps(7, 43)
>>> t2 = Temps(7, 41)
>>> t3 = Temps(7, 37)
>>> total = sum([t1, t2, t3])
>>> print(total)
23:01:00

En général, si toutes les opérations à l'intérieur d'une fonction peuvent être effectuées sur un type donné, la fonction peut être utilisée avec ce type.

Le meilleur type de polymorphisme est celui involontaire, où vous découvrez que vous avez déjà écrit une fonction qui peut être appliquée à un type pour lequel elle n'était pas prévue.

17-10. Débogage

Il est permis d'ajouter des attributs à des objets à tout moment de l'exécution d'un programme, mais si vous avez des objets du même type qui ne possèdent pas les mêmes attributs, il est facile de faire des erreurs. Il est souhaitable d'initialiser tous les attributs d'un objet dans la méthode init.

Si vous n'êtes pas sûr si un objet possède un attribut particulier, vous pouvez utiliser la fonction interne hasattr (voir la section 15.7Débogage).

Une autre façon d'accéder à des attributs est la fonction intégrée vars, qui prend en paramètre un objet et renvoie un dictionnaire qui fait correspondre des noms des attributs (sous forme de chaînes de caractères) à leurs valeurs :

 
Sélectionnez
>>> p = Point(3, 4)
>>> vars(p)
{'y': 4, 'x': 3}

Aux fins de débogage, vous trouverez peut-être utile de retenir cette fonction très pratique :

 
Sélectionnez
def afficher_attributs(obj):
    for attr in vars(obj):
        print(attr, getattr(obj, attr))

afficher_attributs parcourt le dictionnaire et affiche le nom de chaque attribut et sa valeur correspondante.

La fonction interne getattr prend un objet et un nom d'attribut (sous forme de chaîne de caractères) et renvoie la valeur de l'attribut.

17-11. Interface et implémentation

L'un des objectifs de la conception orientée objet est de rendre le logiciel plus facile à maintenir, ce qui signifie que vous pouvez garder le programme en état de fonctionnement lorsque d'autres parties du système sont modifiées, et modifier le programme pour répondre à de nouveaux besoins.

Un principe de conception qui permet d'atteindre cet objectif est de garder les interfaces séparées des implémentations. Pour les objets, cela signifie que les méthodes fournies par une classe ne doivent pas dépendre de la façon dont les attributs sont représentés.

Par exemple, dans ce chapitre nous avons développé une classe qui représente un moment de la journée. Cette classe fournit notamment les méthodes temps_vers_int, est_apres et ajouter_temps.

Nous pouvons mettre en œuvre ces méthodes de plusieurs façons. Les détails de la mise en œuvre dépendent de la façon dont nous représentons le temps. Dans ce chapitre, les attributs d'un objet Temps sont heure, minute et seconde.

Une autre solution serait de remplacer ces attributs par un seul entier représentant le nombre de secondes depuis minuit. Cette mise en œuvre rendrait certaines méthodes, comme est_apres, plus facile à écrire, mais elle rend plus difficile l'écriture d'autres méthodes.

Après avoir déployé une nouvelle classe, vous découvrirez peut-être une meilleure mise en œuvre. Si d'autres parties du programme utilisent votre classe, modifier l'interface peut s'avérer fastidieux et être source d'erreurs.

Mais si vous avez soigneusement conçu l'interface, vous pouvez modifier la mise en œuvre interne sans modifier l'interface, ce qui signifie qu'il n'y aura pas besoin de modifier d'autres parties du programme.

17-12. Glossaire

  • langage orienté objet : un langage qui fournit des fonctionnalités, telles que les types et les méthodes définies par le programmeur, qui facilitent la programmation orientée objet.
  • programmation orientée objet : un style de programmation dans lequel les données et les opérations qui les manipulent sont organisées en classes et méthodes.
  • méthode : une fonction qui est définie à l'intérieur d'une définition de classe et est invoquée sur les instances de cette classe.
  • sujet : l'objet sur lequel une méthode est invoquée.
  • argument positionnel : un argument qui n'inclut pas un nom de paramètre, donc il n'est pas un argument mot-clé.
  • surcharge d'opérateur : modification du comportement d'un opérateur comme + de sorte qu'il prenne en charge un type défini par le programmeur.
  • résolution de méthode basée sur le type : un modèle de programmation qui vérifie le type d'un opérande et invoque des fonctions différentes pour des types différents.
  • polymorphe : relatif à une fonction qui peut être utilisée pour plus d'un type.
  • dissimulation d'information : le principe selon lequel l'interface fournie par un objet ne doit pas dépendre de sa mise en œuvre, en particulier de la représentation de ses attributs.

17-13. Exercices

Exercice 1

Téléchargez le code de ce chapitre à partir de Time2.py. Remplacez les attributs de Time par un seul entier représentant le nombre de secondes écoulées depuis minuit. Puis, modifiez les méthodes (et la fonction int_vers_temps) pour les adapter à la nouvelle mise en œuvre. Vous ne devriez pas avoir à modifier le code de test dans main. Lorsque vous avez terminé, le programme devrait afficher la même chose que précédemment. Solution : Time2_soln.py.

Exercice 2

Cet exercice est un conte pédagogique sur l'une des erreurs les plus courantes et difficiles à trouver en Python. Écrivez une définition pour une classe nommée Kangaroo (kangaroo est le mot anglais pour le kangourou), avec les méthodes suivantes :

  1. Une méthode __init__ qui initialise un attribut nommé pouch_contents à une liste vide (le mot anglais pouch désigne généralement un sac ou une bourse, mais plus précisément ici la poche ventrale dans laquelle la femelle du kangourou abrite son petit) ;
  2. Une méthode nommée put_in_pouch qui prend un objet d'un type quelconque et l'ajoute à pouch_contents  ;
  3. Une méthode __str__ qui renvoie une représentation sous forme de chaîne de caractères de l'objet Kangaroo et du contenu de la poche.

Testez votre code en créant deux objets Kangaroo , en les attribuant à des variables nommées kanga et roo , puis en ajoutant roo au contenu de la poche de kanga .

Téléchargez BadKangaroo.py. Il contient une solution au problème précédent avec un gros bogue bien vicieux. Trouvez et corrigez le bogue.

Si vous vous retrouvez coincé, vous pouvez télécharger GoodKangaroo.py, qui explique le problème et montre une solution.


précédentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par Allen B. Downey et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2016 Developpez.com.