Pensez en Python

Comment maîtriser la science de l'informatique


précédentsommairesuivant

15. Classes et objets

À présent, vous savez comment utiliser les fonctions pour organiser le code et les types internes pour organiser les données. La prochaine étape est d'apprendre « la programmation orientée objet », qui utilise des types définis par le programmeur pour organiser tant le code que les données. La programmation orientée objet est un sujet vaste, il faudra quelques chapitres pour en faire un tour d'horizon.

Les exemples de code de ce chapitre sont disponibles à l'adresse Point1.py ; des solutions aux exercices sont disponibles sur Point1_soln.py.

15-1. Types définis par le programmeur

Nous avons utilisé de nombreux types internes de Python ; maintenant, nous allons définir un nouveau type. À titre d'exemple, nous allons créer un type appelé Point qui représente un point dans l'espace bidimensionnel.

En notation mathématique, les points sont souvent écrits entre parenthèses avec une virgule séparant les coordonnées cartésiennes. Par exemple, (0,0) représente l'origine, et (x, y) représente le point à x unités vers la droite et à y unités vers le haut par rapport à l'origine.

Il existe plusieurs façons de représenter des points en Python :

  • nous pourrions stocker les coordonnées séparément dans deux variables x et y ;
  • nous pourrions stocker les coordonnées comme éléments d'une liste ou d'un tuple ;
  • nous pourrions créer un nouveau type pour représenter les points comme des objets.

La création d'un nouveau type est plus compliquée que les autres options, mais elle présente des avantages qui seront bientôt évidents.

Un type défini par le programmeur s'appelle également une classe. Une définition de classe ressemble à ceci :

 
Sélectionnez
class Point:
    """Représente un point dans l'espace 2-D."""

L'en-tête indique que la nouvelle classe s'appelle Point. Le corps est une docstring qui explique à quoi sert la classe. Vous pouvez définir des variables et des méthodes dans une définition de classe, mais nous y reviendrons plus tard.

La définition d'une classe nommée Point crée un objet classe.

 
Sélectionnez
>>> Point
<class '__main__.Point'>

Comme la classe Point est définie au plus haut niveau, son « nom complet » est :

 
Sélectionnez
__main__.Point.

L'objet classe est une espèce d'usine à créer des objets. Pour créer un Point, vous appelez Point comme si c'était une fonction.

 
Sélectionnez
>>> pt = Point()
>>> pt
<__main__.Point object at 0xb7e9d3ac>

La valeur de retour est une référence à un objet Point, que nous attribuons à pt.

La création d'un nouvel objet s'appelle instanciation, et l'objet est une instance de la classe.

Lorsque vous affichez une instance, Python vous indique à quelle classe elle appartient et où elle est stockée dans la mémoire (le préfixe 0x signifie que le nombre qui suit est en hexadécimal).

Chaque objet est une instance d'une classe, donc « objet » et « instance » sont des termes interchangeables. Mais, dans ce chapitre, j'utilise « instance » pour indiquer que je parle d'un type défini par le programmeur.

15-2. Attributs

Vous pouvez attribuer des valeurs à une instance en utilisant la notation pointée :

 
Sélectionnez
>>> pt.x = 3.0
>>> pt.y = 4.0

Cette syntaxe est similaire à la syntaxe pour sélectionner une variable d'un module, telle que math.pi ou string.whitespace. Dans ce cas, cependant, nous affectons des valeurs aux éléments nommés d'un objet. Ces éléments s'appellent des attributs.

Le schéma suivant montre le résultat de ces affectations. Un diagramme d'état qui montre un objet et ses attributs s'appelle un diagramme d'objets ; voir Figure 15.1.

Image non disponible
Figure 15.1 : Diagramme d'objets.

La variable pt fait référence à un objet Point, qui contient deux attributs. Chaque attribut se réfère à un nombre à virgule flottante.

Vous pouvez lire la valeur d'un attribut en utilisant la même syntaxe :

 
Sélectionnez
>>> pt.y
4.0
>>> x = pt.x
>>> x
3.0

L'expression pt.x signifie, « Va à l'objet référencé par pt et amène la valeur de x. » Dans l'exemple, nous attribuons cette valeur à une variable nommée x. Il n'y a aucun conflit entre la variable x et l'attribut x.

Vous pouvez utiliser la notation pointée dans le cadre d'une expression quelconque. Par exemple :

 
Sélectionnez
>>> '(%g, %g)' % (pt.x, pt.y)
'(3.0, 4.0)'
>>> distance = math.sqrt(pt.x**2 + pt.y**2)
>>> distance
5.0

Vous pouvez passer une instance comme argument de la manière habituelle. Par exemple :

 
Sélectionnez
def afficher_point(p):
    print('(%g, %g)' % (p.x, p.y))

afficher_point prend un point comme argument et l'affiche dans la notation mathématique. Pour l'appeler, vous pouvez passer pt comme argument :

 
Sélectionnez
>>> afficher_point(pt)
(3.0, 4.0)

Dans la fonction, p est un alias pour pt, donc si la fonction modifie p, pt est modifié.

À titre d'exercice, écrivez une fonction appelée distance_entre_points qui prend deux Points comme arguments et renvoie la distance entre eux.

15-3. Rectangles

Parfois, ce que les attributs d'un objet devraient être est évident, mais d'autres fois vous devez prendre des décisions. Par exemple, imaginez que vous concevez une classe pour représenter des rectangles. Quels sont les attributs que vous voudriez utiliser pour spécifier l'emplacement et la taille d'un rectangle ? Vous pouvez ignorer l'angle ; pour faire simple, supposons que le rectangle est soit vertical, soit horizontal.

Il existe au moins deux possibilités :

  • vous pourriez spécifier un coin du rectangle (ou le centre), la largeur et la hauteur ;
  • vous pourriez spécifier deux coins opposés.

À ce stade, il est difficile de dire si l'une est meilleure que l'autre, donc nous allons mettre en œuvre la première, juste à titre d'exemple.

Voici la définition de la classe :

 
Sélectionnez
class Rectangle:
    """Représente un rectangle.

    attributs: largeur, hauteur, coin.
    """

La docstring répertorie les attributs : largeur et hauteur sont des nombres ; coin est un objet Point qui spécifie le coin inférieur gauche.

Pour représenter un rectangle, vous devez instancier un objet Rectangle et attribuer des valeurs aux attributs :

 
Sélectionnez
rect = Rectangle()
rect.largeur = 100.0
rect.hauteur = 200.0
rect.coin = Point()
rect.coin.x = 0.0
rect.coin.y = 0.0

L'expression rect.coin.x signifie, « Va à l'objet auquel se réfère rect et sélectionne l'attribut nommé coin ; puis va à cet objet et sélectionne l'attribut nommé x. »

Image non disponible
Figure 15.2 : Diagramme d'objets.

La figure 15.2 montre l'état de cet objet. Un objet qui est un attribut d'un autre objet est inclus.

15-4. Instances comme valeurs de retour

Les fonctions peuvent renvoyer des instances. Par exemple, trouver_centre prend un Rectangle comme argument et renvoie un Point qui contient les coordonnées du centre du Rectangle :

 
Sélectionnez
def trouver_centre(rectangle):
    p = Point()
    p.x = rectangle.coin.x + rectangle.largeur/2
    p.y = rectangle.coin.y + rectangle.hauteur/2
    return p

Voici un exemple qui passe rect comme argument et attribue le Point résultant à centre :

 
Sélectionnez
>>> centre = trouver_centre(rect)
>>> afficher_point(centre)
(50, 100)

15-5. Les objets sont modifiables

Vous pouvez modifier l'état d'un objet en faisant une affectation à l'un de ses attributs. Par exemple, pour modifier la taille d'un rectangle sans modifier sa position, vous pouvez modifier les valeurs de largeur et de hauteur :

 
Sélectionnez
rect.largeur = rect.largeur + 50
rect.hauteur = rect.hauteur + 100

Vous pouvez également écrire des fonctions qui modifient les objets. Par exemple, agrandir_rectangle prend un objet Rectangle et deux nombres, dLargeur et dHauteur, et ajoute les nombres à la largeur et la hauteur du rectangle :

 
Sélectionnez
def agrandir_rectangle(rectangle, dLargeur, dHauteur):
    rectangle.largeur += dLargeur
    rectangle.hauteur += dHauteur

Voici un exemple qui illustre l'effet :

 
Sélectionnez
>>> rect.largeur, rect.hauteur
(150.0, 300.0)
>>> agrandir_rectangle(rect, 50, 100)
>>> rect.largeur, rect.hauteur
(200.0, 400.0)

À l'intérieur de la fonction, rectangle est un alias pour rect, donc quand la fonction modifie rectangle, rect est modifié.

À titre d'exercice, écrivez une fonction nommée deplacer_rectangle qui prend un Rectangle et deux nombres nommés dx et dy. Elle devrait modifier l'emplacement du rectangle en ajoutant dx à la coordonnée x de coin et dy à la coordonnée y de coin.

15-6. Copier

L'aliasing peut rendre un programme difficile à lire parce que les changements à un seul endroit pourraient avoir des effets inattendus dans un autre endroit. Il est difficile de garder une trace de toutes les variables qui pourraient se référer à un objet donné.

Copier un objet est souvent une solution de rechange à l'aliasing. Le module copy contient une fonction appelée copy qui peut dupliquer un objet quelconque :

 
Sélectionnez
>>> p1 = Point()
>>> p1.x = 3.0
>>> p1.y = 4.0

>>> import copy
>>> p2 = copy.copy(p1)

p1 et p2 contiennent les mêmes données, mais ils ne sont pas le même Point.

 
Sélectionnez
>>> afficher_point(p1)
(3, 4)
>>> afficher_point(p2)
(3, 4)
>>> p1 is p2
False
>>> p1 == p2
False

L'opérateur is indique que p1 et p2 ne sont pas le même objet, comme nous nous y attendions. Mais vous vous attendiez peut-être que l'égalité == soit vraie, parce que ces points contiennent les mêmes données. Dans ce cas, vous serez déçu d'apprendre que pour les instances, le comportement par défaut de l'opérateur == est le même que pour l'opérateur is ; il vérifie l'identité des objets, pas leur équivalence. Cela arrive parce que, pour les types définis par le programmeur, Python ne sait pas ce qui devrait être considéré comme équivalent. Du moins, pas encore.

Si vous utilisez copy.copy pour dupliquer un Rectangle, vous verrez qu'il copie l'objet Rectangle, mais pas le Point inclus.

 
Sélectionnez
>>> rect2 = copy.copy(rect)
>>> rect2 is rect
False
>>> rect2.coin is rect.coin
True
Image non disponible
Figure 15.3 : Diagramme d'objets.

La figure 15.3 montre à quoi ressemble le diagramme d'objet. Cette opération s'appelle une copie superficielle, car elle copie l'objet et toutes les références qu'il contient, mais pas les objets inclus.

Pour la majorité des applications, ce n'est pas ce que vous voulez. Dans cet exemple, l'invocation de agrandir_rectangle sur l'un des Rectangles n'affecterait pas l'autre, mais l'invocation de deplacer_rectangle sur l'un d'eux affecterait tous les deux ! Ce comportement est source de confusion et d'erreurs.

Heureusement, le module copy fournit une méthode nommée deepcopy qui copie non seulement l'objet, mais aussi les objets auxquels il se réfère, et les objets auxquels ces derniers se réfèrent, et ainsi de suite. Vous ne serez pas surpris d'apprendre que cette opération s'appelle une copie en profondeur.

 
Sélectionnez
>>> rect3 = copy.deepcopy(rect)
>>> rect3 is rect
False
>>> rect3.coin is rect.coin
False

rect3 et rect sont des objets complètement séparés.

À titre d'exercice, écrivez une version de deplacer_rectangle qui crée et renvoie un nouveau Rectangle au lieu de modifier l'ancien.

15-7. Débogage

Lorsque vous commencez à travailler avec des objets, vous pourriez rencontrer de nouvelles exceptions. Si vous essayez d'accéder à un attribut qui n'existe pas, vous obtenez une AttributeError :

 
Sélectionnez
>>> p = Point()
>>> p.x = 3
>>> p.y = 4
>>> p.z
AttributeError: Point instance has no attribute 'z'

Si vous n'êtes pas sûr de quel type est un objet, vous pouvez demander :

 
Sélectionnez
>>> type(p)
<class '__main__.Point'>

Vous pouvez également utiliser isinstance pour vérifier si un objet est une instance d'une classe :

 
Sélectionnez
>>> isinstance(p, Point)
True

Si vous n'êtes pas sûr si un objet possède un attribut particulier, vous pouvez utiliser la fonction interne hasattr :

 
Sélectionnez
>>> hasattr(p, 'x')
True
>>> hasattr(p, 'z')
False

Le premier argument peut être un objet quelconque ; le second argument est une chaîne qui contient le nom de l'attribut.

Vous pouvez également utiliser une instruction try pour voir si l'objet possède les attributs dont vous avez besoin :

 
Sélectionnez
try:
    x = p.x
except AttributeError:
    x = 0

Cette approche peut faciliter l'écriture des fonctions qui travaillent avec différents types. Nous reviendrons sur ce sujet dans la section 17.9Polymorphisme.

15-8. Glossaire

  • classe : un type défini par le programmeur. Une définition de classe crée un nouvel objet classe.
  • objet classe : un objet qui contient des informations sur un type défini par le programmeur. L'objet classe peut être utilisé pour créer des instances du type.
  • instance : un objet qui appartient à une classe.
  • instancier : créer un nouvel objet.
  • attribut : une des valeurs nommées associées à un objet.
  • objet inclus : un objet qui est stocké comme un attribut d'un autre objet.
  • copie superficielle : copier le contenu d'un objet, y compris les références à des objets inclus ; mise en œuvre par la fonction copy du module copy.
  • copie en profondeur : copier le contenu d'un objet ainsi que tous les objets inclus, et les objets inclus dans ces derniers, et ainsi de suite ; mise en œuvre par la fonction deepcopy du module copy.
  • diagramme d'objets : un diagramme qui montre les objets, leurs attributs et les valeurs des attributs.

15-9. Exercices

Exercice 1

Écrivez une définition pour une classe nommée Cercle ayant les attributs centre et rayon, où le centre est un objet Point et le rayon est un nombre.

Instanciez un objet Cercle qui représente un cercle ayant son centre à (150, 100) et un rayon de 75.

Écrivez une fonction nommée point_du_cercle qui prend un Cercle et un Point et renvoie Vrai si le Point se trouve dans le cercle ou sur sa circonférence.

Écrivez une fonction nommée rect_du_cercle qui prend un Cercle et un Rectangle et renvoie Vrai si le rectangle se trouve entièrement dans le cercle ou sur sa circonférence.

Écrivez une fonction nommée rect_chevauche_cercle qui prend un Cercle et un Rectangle et renvoie Vrai si l'un des coins du Rectangle se trouve à l'intérieur du cercle. Ou, version plus difficile, retourne Vrai si une partie quelconque du Rectangle se trouve à l'intérieur du cercle.

Solution : Circle.py.

Exercice 2

Écrivez une fonction appelée dessine_rect qui prend un objet Tortue et un Rectangle et utilise la Tortue pour dessiner le Rectangle. Voir le chapitre 4Étude de cas : conception d'une interface pour des exemples utilisant des objets Tortue.

Écrivez une fonction appelée dessine_cercle qui prend une tortue et un cercle et dessine le cercle.

Solution : draw.py.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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.