Aller au contenu

Programmation Python/Classes

Un livre de Wikilivres.


Définition d'une classe élémentaire

[modifier | modifier le wikicode]

Pour créer une nouvelle classe d'objets Python, donc un nouveau type de donnée, on utilise l'instruction "class". Les définitions de classes peuvent être situées n'importe où dans un programme, mais on les placera en général au début (ou bien dans un module à importer).

Par exemple, nous allons maintenant créer un nouveau type composite : le type "Point". Ce type correspondra au concept de point en mathématiques. Dans un espace à deux dimensions, un point est caractérisé par deux nombres (ses coordonnées suivant x et y). En notation mathématique, on représente donc un point par ses deux coordonnées x et y enfermées dans une paire de parenthèses. On parlera par exemple du point (25, 17). Une manière naturelle de représenter un point sous Python serait d'utiliser pour les coordonnées deux valeurs de type float. Nous voudrions cependant combiner ces deux valeurs dans une seule entité, ou un seul objet. Pour y arriver, nous allons définir une classe Point() :

>>> class Point:
        "Définition d'un point mathématique"

Remarquons d'emblée que :

  • L'instruction class est un nouvel exemple d’instruction composée. Ce bloc doit contenir au moins une ligne. Dans notre exemple, cette ligne n'est rien d'autre qu'un simple commentaire. Par convention, si la première ligne suivant l'instruction class est une chaîne de caractères, celle-ci sera considérée comme un commentaire et incorporée automatiquement dans un dispositif de documentation des classes qui fait partie intégrante de Python. Prenez donc l'habitude de toujours placer une chaîne décrivant la classe à cet endroit.
  • Rappelez-vous aussi la convention qui consiste à toujours donner aux classes des noms qui commencent par une majuscule. Dans la suite de ce texte, nous respecterons encore une autre convention qui consiste à associer à chaque nom de classe une paire de parenthèses, comme nous le faisons déjà pour les noms de fonctions.

Nous pouvons dès à présent nous servir de cette classe pour créer des objets de ce type, par instanciation. Créons par exemple un nouvel objet p9.

 Sous Python, on peut donc instancier un objet à l'aide d'une simple instruction d'affectation. D'autres langages imposent l'emploi d'une instruction spéciale, souvent appelée "new" pour bien montrer que l'on crée un nouvel objet à partir d'un moule. Exemple : p9 = new Point().
>>> p9 = Point()

Après cette instruction, la variable p9 contient la référence d'un nouvel objet Point(). Nous pouvons dire également que p9 est une nouvelle instance de la classe Point().

 Comme les fonctions, les classes auxquelles on fait appel dans une instruction doivent toujours être accompagnées de parenthèses (même si aucun argument n'est transmis).

Remarquez bien cependant que la définition d'une classe ne nécessite pas de parenthèses (contrairement à ce qui est de règle lors de la définition des fonctions), sauf si nous souhaitons que la classe en cours de définition dérive d'une autre classe préexistante.

Attributs (ou variables) d'instance

[modifier | modifier le wikicode]

L'objet que nous venons de créer est une coquille vide. Nous pouvons ajouter des composants à cet objet par simple assignation, en utilisant le système de qualification des noms par points.

 Ce système de notation est similaire à celui que nous utilisons pour désigner les variables d'un module, comme par exemple "math.pi" ou "string.uppercase". Les modules peuvent en effet contenir des fonctions, mais aussi des classes et des variables. Essayez par exemple :
import  string
print string.uppercase # ABCDEFGHIJKLMNOPQRSTUVWXYZ
print string.lowercase # abcdefghijklmnopqrstuvwxyz
print string.hexdigits # 0123456789abcdefABCDEF


Complétons la classe précédente avec les coordonnées d'un point :

class Point:
    x = 0
    y = 0

p9 = Point()
p9.x = 3.0
p9.y = 4.0
print (p9.x, p9.y)
(3.0, 4.0)
Schéma de variables d'instance
Schéma de variables d'instance

Les variables ainsi définies sont des attributs de l'objet p9, ou encore des variables d'instance. Elles sont incorporées, ou plutôt encapsulées dans l'objet. Le diagramme d'état ci-contre montre le résultat de ces affectations : la variable p9 contient la référence indiquant l'emplacement mémoire du nouvel objet, qui contient lui-même les deux attributs x et y.

On peut utiliser les attributs d'un objet dans n'importe quelle expression, comme toutes les variables ordinaires :

>>> print p9.x
3.0
>>> print p9.x**2 + p9.y**2	
25.0

Du fait de leur encapsulation dans l'objet, les attributs sont des variables distinctes d'autres variables qui pourraient porter le même nom. Par exemple, l'instruction x = p9.x signifie : « extraire de l'objet référencé par p9 la valeur de son attribut x, et assigner cette valeur à la variable x ».

Il n'y a pas de conflit entre la variable x et l'attribut x de l'objet p9. L'objet p9 contient en effet son propre espace de noms, indépendant de l'espace de nom principal où se trouve la variable x.

 Nous venons de voir qu'il est très aisé d'ajouter un attribut à un objet en utilisant une simple instruction d'assignation telle que p9.x = 3.0. On peut se permettre cela sous Python (c'est une conséquence de l'assignation dynamique des variables), mais cela n'est pas vraiment recommandable. En effet, nous n'utiliserons cette façon de faire uniquement dans le but de simplifier nos explications concernant les attributs d'instances.

Passage d'objets comme arguments lors de l'appel d'une fonction

[modifier | modifier le wikicode]

Les fonctions peuvent utiliser des objets comme paramètres (elles peuvent également fournir un objet comme valeur de retour). Par exemple, vous pouvez définir une fonction telle que celle-ci :

>>> def affiche_point(p):
        print "coord. horizontale =", p.x, "coord. verticale =", p.y

Le paramètre p utilisé par cette fonction doit être un objet de type Point(), puisque l'instruction qui suit utilise les variables d'instance p.x et p.y. Lorsqu'on appelle cette fonction, il faut donc lui fournir un objet de type Point() comme argument. Essayons avec l'objet p9 :

>>> affiche_point(p9)
coord. horizontale = 3.0 coord. verticale = 4.0

Exercices

  1. Écrivez une fonction distance() qui permette de calculer la distance entre deux points. Cette fonction attendra évidemment deux objets Point() comme arguments.

Solution

  1. Réfléchissez !


Similitude et unicité

[modifier | modifier le wikicode]

Dans la langue parlée, les mêmes mots peuvent avoir des significations fort différentes suivant le contexte dans lequel on les utilise. La conséquence en est que certaines expressions utilisant ces mots peuvent être comprises de plusieurs manières différentes (expressions ambiguës).

Le mot « même », par exemple, a des significations différentes dans les phrases : « Charles et moi avons la même voiture » et « Charles et moi avons la même mère ». Dans la première, ce que je veux dire est que la voiture de Charles et la mienne sont du même modèle. Il s'agit pourtant de deux voitures distinctes. Dans la seconde, j'indique que la mère de Charles et la mienne constituent en fait une seule et unique personne.

Lorsque nous traitons d'objets logiciels, nous pouvons rencontrer la même ambiguïté. Par exemple, si nous parlons de l'égalité de deux objets Point(), cela signifie-t-il que ces deux objets contiennent les mêmes données (leurs attributs), ou bien cela signifie-t-il que nous parlons de deux références à un même et unique objet ? Considérez par exemple les instructions suivantes :

>>> p1 = Point()
>>> p1.x = 3
>>> p1.y = 4
>>> p2 = Point()
>>> p2.x = 3
>>> p2.y = 4
>>> print (p1 == p2)
0

Ces instructions créent deux objets p1 et p2 qui restent distincts, même s'ils ont des contenus similaires. La dernière instruction teste l'égalité de ces deux objets (double signe égale), et le résultat est zéro (ce qui signifie que l'expression entre parenthèses est fausse : il n'y a donc pas égalité).

On peut confirmer cela d'une autre manière encore :

>>> print p1
<__main__.Point instance at 00C2CBEC>
>>> print p2
<__main__.Point instance at 00C50F9C>

L'information est claire : les deux variables p1 et p2 référencent bien des objets différents.


Essayons autre chose, à présent :

>>> p2 = p1
>>> print (p1 == p2)
1

Par l'instruction p2 = p1, nous assignons le contenu de p1 à p2. Cela signifie que désormais ces deux variables référencent le même objet. Les variables p1 et p2 sont des alias l'une de l'autre.

Le test d'égalité dans l'instruction suivante renvoie cette fois la valeur 1, ce qui signifie que l'expression entre parenthèses est vraie : p1 et p2 désignent bien toutes deux un seul et unique objet, comme on peut s'en convaincre en essayant encore :

>>> p1.x = 7
>>> print p2.x
7
>>> print p1
<__main__.Point instance at 00C2CBEC>
>>> print p2
<__main__.Point instance at 00C2CBEC>

Objets composés d'objets

[modifier | modifier le wikicode]

Supposons maintenant que nous voulions définir une classe pour représenter des rectangles. Pour simplifier, nous allons considérer que ces rectangles seront toujours orientés horizontalement ou verticalement, et jamais en oblique.

De quelles informations avons-nous besoin pour définir de tels rectangles ? Il existe plusieurs possibilités. Nous pourrions par exemple spécifier la position du centre du rectangle (deux coordonnées) et préciser sa taille (largeur et hauteur). Nous pourrions aussi spécifier les positions du coin supérieur gauche et du coin inférieur droit. Ou encore la position du coin supérieur gauche et la taille. Admettons ce soit cette dernière méthode qui soit retenue.

Définissons donc notre nouvelle classe :

>>> class Rectangle:
        "définition d'une classe de rectangles"

... et servons nous-en tout de suite pour créer une instance :

>>> boite = Rectangle()
>>> boite.largeur = 50.0
>>> boite.hauteur = 35.0

Nous créons ainsi un nouvel objet Rectangle() et deux attributs. Pour spécifier le coin supérieur gauche, nous allons utiliser une instance de la classe Point() que nous avons définie précédemment. Ainsi nous allons créer un objet à l'intérieur d'un autre objet !

>>> boite.coin = Point()
>>> boite.coin.x = 12.0
>>> boite.coin.y = 27.0

Pour accéder à un objet qui se trouve à l'intérieur d'un autre objet, on utilise la qualification des noms hiérarchisée (à l'aide de points) que nous avons déjà rencontrée à plusieurs reprises. Ainsi l'expression boite.coin.y signifie « Aller à l'objet référencé dans la variable boite. Dans cet objet, repérer l'attribut coin, puis aller à l'objet référencé dans cet attribut. Une fois cet autre objet trouvé, sélectionner son attribut y. »

Vous pourrez peut-être mieux vous représenter à l'avenir les objets composites, à l'aide de diagrammes similaires à celui que nous reproduisons ci-dessous :

schéma de variables dans un contexte objet
schéma de variables dans un contexte objet

Le nom « boîte » se trouve dans l'espace de noms principal. Il référence un autre espace de noms réservé à l'objet correspondant, dans lequel sont mémorisés les noms « largeur », « hauteur » et « coin ». Ceux-ci référencent à leur tour, soit d'autres espaces de noms (cas du nom « coin »), soit des valeurs bien déterminées. Python réserve des espaces de noms différents pour chaque module, chaque classe, chaque instance, chaque fonction. Vous pouvez tirer parti de tous ces espaces bien compartimentés afin de réaliser des programmes robustes, c'est-à-dire des programmes dont les différents composants ne peuvent pas facilement interférer.


Objets comme valeurs de retour d'une fonction

[modifier | modifier le wikicode]

Nous avons vu plus haut que les fonctions peuvent utiliser des objets comme paramètres. Elles peuvent également transmettre une instance comme valeur de retour. Par exemple, la fonction trouveCentre() ci-dessous doit être appelée avec un argument de type Rectangle() et elle renvoie un objet Point(), lequel contiendra les coordonnées du centre du rectangle.

>>> def trouveCentre(box):
        p = Point()
        p.x = box.coin.x + box.largeur/2.0
        p.y = box.coin.y + box.hauteur/2.0
        return p

Pour appeler cette fonction, vous pouvez utiliser l'objet boite comme argument :

>>> centre = trouveCentre(boite)
>>> print centre.x, centre.y
37.0  44.5

Les objets sont modifiables

[modifier | modifier le wikicode]

Nous pouvons changer les propriétés d'un objet en assignant de nouvelles valeurs à ses attributs. Par exemple, nous pouvons modifier la taille d'un rectangle (sans modifier sa position), en réassignant ses attributs hauteur et largeur :

>>> boite.hauteur = boite.hauteur + 20
>>> boite.largeur = boite.largeur – 5

Nous pouvons faire cela sous Python, parce que dans ce langage les propriétés des objets sont toujours publiques (du moins dans la version actuelle 2.0). D'autres langages établissent une distinction nette entre attributs publics (accessibles de l'extérieur de l'objet) et attributs privés (qui sont accessibles seulement aux algorithmes inclus dans l'objet lui-même).

Comme nous l'avons déjà signalé plus haut (à propos de la définition des attributs par assignation simple, depuis l'extérieur de l'objet), modifier de cette façon les attributs d'une instance n'est pas une pratique recommandable, parce qu'elle contredit l'un des objectifs fondamentaux de la programmation orientée objet, qui vise à établir une séparation stricte entre la fonctionnalité d'un objet (telle qu'elle a été déclarée au monde extérieur) et la manière dont cette fonctionnalité est réellement implémentée dans l'objet (et que le monde extérieur n'a pas à connaître).

Plus concrètement, nous devrons veiller désormais à ce que les objets que nous créons ne soient modifiables en principe que par l'intermédiaire de méthodes mises en place spécifiquement dans ce but, comme nous allons l'expliquer dans le chapitre suivant.


Définition d'une méthode

[modifier | modifier le wikicode]

Pour illustrer notre propos, nous allons définir une nouvelle classe Time, qui nous permettra d'effectuer toute une série d'opérations sur des instants, des durées, etc. :

>>> class Time:
        "Définition d'une classe temporelle"

Créons à présent un objet de ce type, et ajoutons-lui des variables d'instance pour mémoriser les heures, minutes et secondes :

>>> instant = Time()
>>> instant.heure = 11
>>> instant.minute = 34
>>> instant.seconde = 25

À titre d'exercice, écrivez maintenant vous-même une fonction affiche_heure(), qui serve à visualiser le contenu d'un objet de classe Time() sous la forme conventionnelle « heure:minute:seconde ».

Appliquée à l'objet instant créé ci-dessus, cette fonction devrait donc afficher 11:34:25 :

>>> print affiche_heure(instant)
11:34:25

Votre fonction ressemblera probablement à ceci :

>>> def affiche_heure(t):
        print str(t.heure) + ":" + str(t.minute) + ":" + str(t.seconde)

(Notez au passage l'utilisation de la fonction str() pour convertir les données numériques en chaînes de caractères). Si par la suite vous utilisez fréquemment des objets de la classe Time(), il y a gros à parier que cette fonction d'affichage vous sera fréquemment utile.

Il serait donc probablement fort judicieux d'encapsuler cette fonction affiche_heure() dans la classe Time() elle-même, de manière à s'assurer qu'elle soit toujours automatiquement disponible chaque fois que l'on doit manipuler des objets de la classe Time().

Une fonction qui est ainsi encapsulée dans une classe s'appelle une méthode.


Définition concrète d'une méthode

On définit une méthode comme on définit une fonction, avec cependant deux différences :

  • La définition d'une méthode est toujours placée à l'intérieur de la définition d'une classe, de manière à ce que la relation qui lie la méthode à la classe soit clairement établie.
  • Le premier paramètre utilisé par une méthode doit toujours être une référence d'instance. Vous pourriez en principe utiliser un nom de variable quelconque pour ce paramètre, mais il est vivement conseillé de respecter la convention qui consiste à toujours lui donner le nom self. Le paramètre self désigne donc l'instance à laquelle la méthode sera associée, dans les instructions faisant partie de la définition. (De ce fait, la définition d'une méthode comporte toujours au moins un paramètre, alors que la définition d'une fonction peut n'en comporter aucun).

Voyons comment cela se passe en pratique :

Pour ré-écrire la fonction affiche_heure() comme une méthode de la classe Time(), il nous suffit de déplacer sa définition à l'intérieur de celle de la classe, et de changer le nom de son paramètre :

>>> class Time:
        "Nouvelle classe temporelle"
        def affiche_heure(self):
            print str(self.heure) + ":" + str(self.minute) \
                  + ":" + str(self.seconde)

La définition de la méthode fait maintenant partie du bloc d'instructions indentées après l'instruction class. Notez bien l'utilisation du mot réservé self, qui se réfère donc à toute instance susceptible d'être créée à partir de cette classe.


Essai de la méthode dans une instance

Nous pouvons dès à présent instancier un objet de notre nouvelle classe Time() :

>>> maintenant = Time()

Si nous essayons d'utiliser un peu trop vite notre nouvelle méthode, ça ne marche pas :

>>> maintenant.affiche_heure()
AttributeError: 'Time' instance has no attribute 'heure'

C'est normal : nous n'avons pas encore créé les attributs d'instance. Il faudrait faire par exemple :

>>> maintenant.heure = 13
>>> maintenant.minute = 34
>>> maintenant.seconde = 21
>>> maintenant.affiche_heure()
13:34:21

Nous avons cependant déjà signalé à plusieurs reprises qu'il n'est pas recommandable de créer ainsi les attributs d'instance en dehors de l'objet lui-même, ce qui conduit (entre autres désagréments) à des erreurs comme celle que nous venons de rencontrer, par exemple.

Voyons donc à présent comment nous pouvons mieux faire.

Méthodes prédéfinies

[modifier | modifier le wikicode]

Certaines méthodes de classe Python existent automatiquement dans toutes les classes sans être déclarées, et certaines se lancent automatiquement lors de certains événements. Ces méthodes spéciales sont nommées entre deux underscores (__)[1].


Comme pour les fonctions, la chaîne de documentation est définie dans cette méthode.

>>> print p9.__doc__
Définition d'un point mathématique

Dans le cas d'un module, la doc correspond au premier bloc de commentaire :

"""Définition d'un point mathématique"""

Cette méthode permet de lancer des recherches dans une classes comme dans un objet composite comme la liste, avec "in". En effet, il suffit d'y placer les getters sur les attributs :

class MyClass:
    attribute1 = 'ok'

    def __contains__(self, attribute):
        if self.attribute1: return True

MaClasse1 = MyClass()
print attribute1 in MaClasse1 # True
print attribute2 in MaClasse1 # False

Destructeur : se lance quand l'objet est détruit.


__enter__ et __exit__

[modifier | modifier le wikicode]

Respectivement constructeur et destructeur des classes instanciées avec with, exécutés respectivement après et avant __init__ et __del__. Exemple :

class Test:        
    def __enter__(self):
        print 'enter'
     
    def __exit__(self, exc_type, exc_value, traceback):
        print 'exit'

with Test():
    pass
enter
exit


Cette méthode se lance lors du premier accès à la classe.

Exemple :

class Complexe:
	def __init__(self, r, i):
		self.reel = r
		self.imaginaire = i

Complexe1 = Complexe(1, 2)
print Complexe1.reel    # Affiche 1


L'erreur que nous avons rencontrée au paragraphe précédent est-elle évitable ? Elle ne se produirait effectivement pas, si nous nous étions arrangés pour que la méthode affiche_heure() puisse toujours afficher quelque chose, sans qu'il ne soit nécessaire d'effectuer au préalable aucune manipulation sur l'objet nouvellement créé. En d'autres termes, il serait judicieux que les variables d'instance soient prédéfinies elles aussi à l'intérieur de la classe, avec pour chacune d'elles une valeur « par défaut ».

Pour obtenir cela, nous allons faire appel à une méthode particulière, que l'on appelle un constructeur. Une méthode constructeur est une méthode qui est exécutée automatiquement lorsque l'on instancie un nouvel objet à partir de la classe. On peut y placer tout ce qui semble nécessaire pour initialiser automatiquement l'objet que l'on crée.

Exemple :

>>> class Time:
        "Encore une nouvelle classe temporelle"
        def __init__(self):
            self.heure =0
            self.minute =0
            self.seconde =0

        def affiche_heure(self):
            print str(self.heure) + ":" + str(self.minute) \
                 + ":" + str(self.seconde)

>>> tstart = Time()
>>> tstart.affiche_heure()
0:0:0

L'intérêt de cette technique apparaîtra plus clairement si nous ajoutons encore quelque chose. Comme toute méthode qui se respecte, la méthode __init__() peut être dotée de paramètres. Ceux-ci vont jouer un rôle important, parce qu'ils vont permettre d'instancier un objet et d'initialiser certaines de ses variables d'instance, en une seule opération. Dans l'exemple ci-dessus, veuillez donc modifier la définition de la méthode __init__() comme suit :

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

La méthode __init__() comporte à présent 3 paramètres, avec pour chacun une valeur par défaut. Pour lui transmettre les arguments correspondants, il suffit de placer ceux-ci dans les parenthèses qui accompagnent le nom de la classe, lorsque l'on écrit l'instruction d'instanciation du nouvel objet.

Voici par exemple la création et l'initialisation simultanées d'un nouvel objet Time() :

>>> recreation = Time(10, 15, 18)
>>> recreation.affiche_heure()
10:15:18

Puisque les variables d'instance possèdent maintenant des valeurs par défaut, nous pouvons aussi bien créer de tels objets Time() en omettant un ou plusieurs arguments :

>>> rentree = Time(10, 30)
>>> rentree.affiche_heure()
10:30:0
>>> print p9
<__main__.Point instance at 0x403e1a8c>

Le message renvoyé par Python indique, comme vous l'aurez certainement bien compris tout de suite, que "p9" est une instance de la classe "Point()", qui est définie elle-même au niveau principal du programme. Elle est située dans un emplacement bien déterminé de la mémoire vive, dont l'adresse apparaît ici en notation hexadécimale.


Constructeur de métaclasse.

La méthode __new__ est statique. Elle est appelée pour créer l'objet instancié avec l'opérateur new, juste avant l'appel à la méthode __init__ initialisant les membres de l'objet créé.

__new__(class, *args, **kwargs)

Les arguments de la méthode __new__ sont décris ci-après :

class
Le premier argument est la classe de l'objet à créer.
args
Liste itérable des arguments non nommés à transmettre au constructeur.
kwargs
Dictionnaire des arguments nommés à transmettre au constructeur.

La méthode retourne l'objet créé. Comme cette méthode est héritée de la classe object, une classe peut la surcharger en appelant celle de la classe parente (super().__new__()) avec des arguments modifiés, ou pour exécuter des actions avant ou après la création d'un objet.

Techniquement, la méthode object.__new__() peut être appelée pour créer un objet, mais il faut ensuite appeler manuellement la méthode __init__() sur l'objet créé. Cet enchaînement des deux appels n'est fait que par l'opérateur new.

Exemple : Une classe héritant de celle des entiers pour créer des cubes d'entier.

class CubeEntier(int):
    def __new__(cls, value):
        return super().__new__(cls, value ** 3)

x = CubeEntier(3)
print(x)  # 27

Cette méthode renvoie une représentation de l'objet quand on l'appelle directement (sans chercher à accéder à ses attributs ou méthodes). Exemple :

class Bar:
    def __init__ (self, iamthis):
        self.iamthis = iamthis

    def __repr__(self):
        return "Bar('%s')" % self.iamthis

bar = Bar('apple')
print bar
Bar('apple')

Dans cette méthode, cet objet renverrait :

<__main__.Bar instance at 0x7f282bbf2a28>


Renvoie une chaine de caractères quand on traite l'objet comme tel. Exemple :

class Bar:
    def __init__ (self, iam_this):
        self.iam_this = iam_this

    def __str__ (self):
        return self.iam_this

bar = Bar('apple')
print bar
apple


Réservé à Python 2.x.


Opérateurs binaires

[modifier | modifier le wikicode]
Fonction Opérateur
__add__ A + B
__sub__ A - B
__mul__ A * B
__truediv__ A / B
__floordiv__ A // B
__mod__ A % B
__pow__ A ** B
__and__ A & B
__or__ A | B
__xor__ A ^ B
__eq__ A == B
__ne__ A != B
__gt__ A > B
__lt__ A < B
__ge__ A >= B
__le__ A <= B
__lshift__ A << B
__rshift__ A >> B
__contains__ A in B
A not in B


Opérateurs unaires

[modifier | modifier le wikicode]
Fonction Opérateur
__pos__ +A
__neg__ -A
__inv__ ~A
__abs__ abs(A)
__len__ len(A)


Gestion des attributs

[modifier | modifier le wikicode]

Getters et setters.

Fonction Forme indirecte Forme directe
__getattr__ getattr(A, B) A.B
__setattr__ setattr(A, B, C) A.B = C
__delattr__ delattr(A, B) del A.B

Gestion des indices

[modifier | modifier le wikicode]

Se déclenchent lorsque l'on manipule un objet comme un dictionnaire[2].

Fonction Opérateur
__getitem__ C[i]
__setitem__ C[i] = v
__delitem__ del C[i]
__getslice__ C[s:e]
__setslice__ C[s:e] = v
__delslice__ del C[s:e]


Fonction Opérateur
__cmp__ cmp(x, y)
__hash__ hash(x)
__nonzero__ bool(x)
__call__ f(x)
__iter__ iter(x)
__reversed__ reversed(x) (2.6+)
__divmod__ divmod(x, y)
__int__ int(x)
__long__ long(x)
__float__ float(x)
__complex__ complex(x)
__hex__ hex(x)
__oct__ oct(x)
__index__
__copy__ copy.copy(x)
__deepcopy__ copy.deepcopy(x)
__sizeof__ sys.getsizeof(x) (2.6+)
__trunc__ math.trunc(x) (2.6+)
__format__ format(x, ...) (2.6+)


Espaces de noms des classes et instances

[modifier | modifier le wikicode]

Les variables définies à l'intérieur d'une fonction sont des variables locales, inaccessibles aux instructions qui se trouvent à l'extérieur de la fonction. Cela permet d'utiliser les mêmes noms de variables dans différentes parties d'un programme, sans risque d'interférence.

Pour décrire la même chose en d'autres termes, nous pouvons dire que chaque fonction possède son propre espace de noms, indépendant de l'espace de noms principal.

Les instructions se trouvant à l'intérieur d'une fonction peuvent accéder aux variables définies au niveau principal, mais en lecture seulement : elles peuvent utiliser les valeurs de ces variables, mais pas les modifier (à moins de faire appel à l'instruction global).

Il existe donc une sorte de hiérarchie entre les espaces de noms. Nous allons constater la même chose à propos des classes et des objets. En effet :

  • Chaque classe possède son propre espace de noms. Les variables qui en font partie sont appelées les attributs de la classe.
  • Chaque objet instance (créé à partir d'une classe) obtient son propre espace de noms. Les variables qui en font partie sont appelées variables d'instance ou attributs d'instance.
  • Les classes peuvent utiliser (mais pas modifier) les variables définies au niveau principal.
  • Les instances peuvent utiliser (mais pas modifier) les variables définies au niveau de la classe et les variables définies au niveau principal.

Considérons par exemple la classe Time() définie précédemment. À la page précédente, nous avons instancié deux objets de cette classe : recreation et rentree. Chacun a été initialisé avec des valeurs différentes, indépendantes. Nous pouvons modifier et réafficher ces valeurs à volonté dans chacun de ces deux objets, sans que l'autre n'en soit affecté :

>>> recreation.heure = 12
>>> rentree.affiche_heure()
10:30:0
>>> recreation.affiche_heure()
12:15:18

Veuillez à présent encoder et tester l'exemple ci-dessous :

>>> class Espaces:                          # 1
        aa = 33                             # 2
        def affiche(self):                  # 3
            print aa, Espaces.aa, self.aa   # 4

>>> aa = 12                                 # 5
>>> essai = Espaces()                       # 6
>>> essai.aa = 67                           # 7
>>> essai.affiche()                         # 8
12 33 67
>>> print aa, Espaces.aa, essai.aa          # 9
12 33 67

Dans cet exemple, le même nom aa est utilisé pour définir trois variables différentes : une dans l'espace de noms de la classe (à la ligne 2), une autre dans l'espace de noms principal (à la ligne 5), et enfin une dernière dans l'espace de nom de l'instance (à la ligne 7).

La ligne 4 et la ligne 9 montrent comment vous pouvez accéder à ces trois espaces de noms (de l'intérieur d'une classe, ou au niveau principal), en utilisant la qualification par points. Notez encore une fois l'utilisation de self pour désigner l'instance.

Les classes constituent le principal outil de la programmation orientée objet ou POO (Object Oriented Programming ou OOP en anglais), qui est considérée de nos jours comme la technique de programmation la plus performante. L'un des principaux atouts de ce type de programmation réside dans le fait que l'on peut toujours se servir d'une classe préexistante pour en créer une nouvelle qui possédera quelques fonctionnalités différentes ou supplémentaires. Le procédé s'appelle dérivation. Il permet de créer toute une hiérarchie de classes allant du général au particulier.

Nous pouvons par exemple définir une classe Mammifere(), qui contiendra un ensemble de caractéristiques propres à ce type d'animal. À partir de cette classe, nous pourrons alors dériver une classe Primate(), une classe Rongeur(), une classe Carnivore(), etc., qui hériteront toutes des caractéristiques de la classe Mammifere(), en y ajoutant leurs spécificités.

Au départ de la classe Carnivore(), nous pourrons ensuite dériver une classe Belette(), une classe Loup(), une classe Chien(), etc., qui hériteront encore une fois toutes les caractéristiques de la classe Mammifere avant d'y ajouter les leurs. Exemple :

>>> class Mammifere:
        caract1 = "il allaite ses petits ;"

>>> class Carnivore(Mammifere):
        caract2 = "il se nourrit de la chair de ses proies ;"

>>> class Chien(Carnivore):
        caract3 = "son cri s'appelle aboiement ;"

>>> mirza = Chien()
>>> print mirza.caract1, mirza.caract2, mirza.caract3
il allaite ses petits ; il se nourrit de la chair de ses proies ;
son cri s'appelle aboiement ;

Dans cet exemple, nous voyons que l'objet mirza, qui est une instance de la classe Chien(), hérite non seulement l'attribut défini pour cette classe, mais également des attributs définis pour les classes parentes.

Vous voyez également dans cet exemple comment il faut procéder pour dériver une classe à partir d'une classe parente : On utilise l'instruction class, suivie comme d'habitude du nom que l'on veut attribuer à la nouvelle classe, et on place entre parenthèses le nom de la classe parente.

Notez bien que les attributs utilisés dans cet exemple sont des attributs des classes (et non des attributs d'instances). L'instance mirza peut accéder à ces attributs, mais pas les modifier :

>>> mirza.caract2 = "son corps est couvert de poils"
>>> print mirza.caract2
son corps est couvert de poils
>>> fido = Chien()
>>> print fido.caract2
il se nourrit de la chair de ses proies ;

Dans ce nouvel exemple, la ligne 1 ne modifie pas l'attribut caract2 de la classe Carnivore(), contrairement à ce que l'on pourrait penser au vu de la ligne 3. Nous pouvons le vérifier en créant une nouvelle instance fido (lignes 4 à 6).

Si vous avez bien assimilé les paragraphes précédents, vous aurez compris que l'instruction de la ligne 1 crée une nouvelle variable d'instance associée seulement à l'objet mirza. Il existe donc dès ce moment deux variables avec le même nom caract2 : l'une dans l'espace de noms de l'objet mirza, et l'autre dans l'espace de noms de la classe Carnivore().

Comment faut-il alors interpréter ce qui s'est passé aux lignes 2 et 3 ? Comme nous l'avons vu plus haut, l'instance mirza peut accéder aux variables situées dans son propre espace de noms, mais aussi à celles qui sont situées dans les espaces de noms de toutes les classes parentes. S'il existe des variables aux noms identiques dans plusieurs de ces espaces, laquelle sera-t-elle sélectionnée lors de l'exécution d'une instruction comme celle de la ligne 2 ?

Pour résoudre ce conflit, Python respecte une règle de priorité fort simple. Lorsqu'on lui demande d'utiliser la valeur d'une variable nommée alpha, par exemple, il commence par rechercher ce nom dans l'espace local (le plus « interne », en quelque sorte). Si une variable alpha est trouvée dans l'espace local, c'est celle-là qui est utilisée, et la recherche s'arrête. Sinon, Python examine l'espace de noms de la structure parente, puis celui de la structure grand-parente, et ainsi de suite jusqu'au niveau principal du programme.

À la ligne 2 de notre exemple, c'est donc la variable d'instance qui sera utilisée. À la ligne 5, par contre, c'est seulement au niveau de la classe grand-parente qu'une variable répondant au nom caract2 peut être trouvée. C'est donc celle-là qui est affichée.

Héritage et polymorphisme

[modifier | modifier le wikicode]

Pour bien comprendre ce script, il faut cependant d'abord vous rappeler quelques notions élémentaires de chimie. Dans votre cours de chimie, vous avez certainement dû apprendre que les atomes sont des entités constituées d'un certain nombre de protons (particules chargées d'électricité positive), d'électrons (chargés négativement) et de neutrons (neutres).

Le type d'atome (ou élément) est déterminé par le nombre de protons, que l'on appelle également numéro atomique. Dans son état fondamental, un atome contient autant d'électrons que de protons, et par conséquent il est électriquement neutre. Il possède également un nombre variable de neutrons, mais ceux-ci n'influencent en aucune manière la charge électrique globale.

Dans certaines circonstances, un atome peut gagner ou perdre des électrons. Il acquiert de ce fait une charge électrique globale, et devient alors un ion (il s'agit d'un ion négatif si l'atome a gagné un ou plusieurs électrons, et d'un ion positif s'il en a perdu). La charge électrique d'un ion est égale à la différence entre le nombre de protons et le nombre d'électrons qu'il contient.

Le script suivant génère des objets atome et des objets ion. Nous avons rappelé ci-dessus qu'un ion est simplement un atome modifié. Dans notre programmation, la classe qui définit les objets « ion » sera donc une classe dérivée de la classe atome : elle héritera d'elle tous ses attributs et toutes ses méthodes, en y ajoutant les siennes propres. On pourra dire également que la méthode affiche() a été surchargée.

L'une de ces méthodes ajoutées (la méthode affiche()) remplace une méthode de même nom héritée de la classe atome. Les classes « atome » et « ion » possèdent donc chacune une méthode de même nom, mais qui effectuent un travail différent. On parle dans ce cas de polymorphisme. On pourra dire également que la méthode affiche() a été surchargée.

Il sera évidemment possible d'instancier un nombre quelconque d'atomes et d'ions à partir de ces deux classes. Or l'une d'entre elles (la classe atome) doit contenir une version simplifiée du tableau périodique des éléments (tableau de Mendeleïev), de façon à pouvoir attribuer un nom d'élément chimique, ainsi qu'un nombre de neutrons, à chaque objet généré. Comme il n'est pas souhaitable de recopier tout ce tableau dans chacune des instances, nous le placerons dans un attribut de classe. Ainsi ce tableau n'existera qu'en un seul endroit en mémoire, tout en restant accessible à tous les objets qui seront produits à partir de cette classe.

Voyons concrètement comment toutes ces idées s'articulent :

class Atome:
    """atomes simplifiés, choisis parmi les 10 premiers éléments du TP""" 
    table = [None, ('hydrogène',0), ('hélium',2), ('lithium',4),
            ('béryllium',5), ('bore',6), ('carbone',6), ('azote',7),
            ('oxygène',8), ('fluor',10), ('néon',10)]
            
    def __init__(self, nat):
        "le n° atomique détermine le n. de protons, d'électrons et de neutrons" 
        self.np, self.ne = nat, nat       # nat = numéro atomique
        self.nn = Atome.table[nat][1]     # nb. de neutrons trouvés dans table
        
    def affiche(self):
        print
        print "Nom de l'élément :", Atome.table[self.np][0]
        print "%s protons, %s électrons, %s neutrons" % \
                  (self.np, self.ne, self.nn)
               
class Ion(Atome):
    """les ions sont des atomes qui ont gagné ou perdu des électrons"""
     
    def __init__(self, nat, charge):
        "le n° atomique et la charge électrique déterminent l'ion"
        Atome.__init__(self, nat)
        self.ne = self.ne - charge
        self.charge = charge
    
    def affiche(self):
        "cette méthode remplace celle héritée de la classe parente" 
        Atome.affiche(self)			# ... tout en l'utilisant elle-même ! 
        print "Particule électrisée. Charge =", self.charge        
        
### Programme principal : ###     

a1 = Atome(5)
a2 = Ion(3, 1)
a3 = Ion(8, -2)
a1.affiche()
a2.affiche()
a3.affiche()

L'exécution de ce script provoque l'affichage suivant :

Nom de l'élément : bore
5 protons, 5 électrons, 6 neutrons

Nom de l'élément : lithium
3 protons, 2 électrons, 4 neutrons
Particule électrisée. Charge = 1

Nom de l'élément : oxygène
8 protons, 10 électrons, 8 neutrons
Particule électrisée. Charge = -2

Au niveau du programme principal, vous pouvez constater que l'on instancie les objets Atome() en fournissant leur numéro atomique (lequel doit être compris entre 1 et 10). Pour instancier des objets Ion(), par contre, on doit fournir un numéro atomique et une charge électrique globale (positive ou négative). La même méthode affiche() fait apparaître les propriétés de ces objets, qu'il s'agisse d'atomes ou d'ions, avec dans le cas de l'ion une ligne supplémentaire (polymorphisme).

Commentaires

La définition de la classe Atome() commence par l'assignation de la variable table. Une variable définie à cet endroit fait partie de l'espace de noms de la classe. C'est donc un attribut de classe, dans lequel nous plaçons une liste d'informations concernant les 10 premiers éléments du tableau périodique de Mendeleïev. Pour chacun de ces éléments, la liste contient un tuple : (nom de l'élément, nombre de neutrons), à l'indice qui correspond au numéro atomique. Comme il n'existe pas d'élément de numéro atomique zéro, nous avons placé à l'indice zéro dans la liste, l'objet spécial None (a priori, nous aurions pu placer à cet endroit n'importe quelle autre valeur, puisque cet indice ne sera pas utilisé. L'objet None de Python nous semble cependant particulièrement explicite).

Viennent ensuite les définitions de deux méthodes :

  • Le constructeur __init__() sert essentiellement ici à générer trois attributs d'instance, destinés à mémoriser respectivement les nombres de protons, d'électrons et de neutrons pour chaque objet atome construit à partir de cette classe (Les attributs d'instance sont des variables liées à self).

Notez bien la technique utilisée pour obtenir le nombre de neutrons à partir de l'attribut de classe, en mentionnant le nom de la classe elle-même dans une qualification par points.

  • La méthode affiche() utilise à la fois les attributs d'instance, pour retrouver les nombres de protons, d'électrons et de neutrons de l'objet courant, et l'attribut de classe (lequel est commun à tous les objets) pour en extraire le nom d'élément correspondant. Veuillez aussi remarquer au passage l'utilisation de la technique de formatage des chaînes.

La définition de la classe Ion() comporte des parenthèses. Il s'agit donc d'une classe dérivée, sa classe parente étant bien entendu la classe Atome() qui précède.

Les méthodes de cette classe sont des variantes de celles de la classe Atome(). Elles devront donc vraisemblablement faire appel à celles-ci. Cette remarque est importante :

 Comment peut-on, à l'intérieur de la définition d'une classe, faire appel à une méthode définie dans une autre classe ?

Il ne faut pas perdre de vue, en effet, qu'une méthode se rattache toujours à l'instance qui sera générée à partir de la classe (instance représentée par self dans la définition). Si une méthode doit faire appel à une autre méthode définie dans une autre classe, il faut pouvoir lui transmettre la référence de l'instance à laquelle elle doit s'associer. Comment faire ? C'est très simple :

 Lorsque dans la définition d'une classe, on souhaite faire appel à une méthode définie dans une autre classe, on doit lui transmettre la référence de l'instance comme premier argument.

C'est ainsi que dans notre script, par exemple, la méthode affiche() de la classe Ion() peut faire appel à la méthode affiche() de la classe Atome() : les informations affichées seront bien celles de l'objet-ion courant, puisque sa référence a été transmise dans l'instruction d'appel :

Atome.affiche(self)

(dans cette instruction, self est bien entendu la référence de l'instance courante).

De la même manière, la méthode constructeur de la classe Ion() fait appel à la méthode constructeur de sa classe parente, dans :

Atome.__init__(self, nat)
Schéma résumant le fonctionnement d'une classe
Schéma résumant le fonctionnement d'une classe


Exercices

  1. Définissez une classe Domino() qui permette d'instancier des objets simulant les pièces d'un jeu de dominos. Le constructeur de cette classe initialisera les valeurs des points présents sur les deux faces A et B du domino (valeurs par défaut = 0). Deux autres méthodes seront définies :
    • une méthode affiche_points() qui affiche les points présents sur les deux faces ;
    • une méthode valeur() qui renvoie la somme des points présents sur les deux faces.
    Exemples d'utilisation de cette classe :
    >>> d1 = Domino(2,6)
    >>> d2 = Domino(4,3)
    >>> d1.affiche_points()
    face A : 2  face B : 6
    >>> d2.affiche_points()
    face A : 4  face B : 3
    >>> print "total des points :", d1.valeur() + d2.valeur()
    total des points : 15
    >>> liste_dominos = []
    >>> for i in range(7):
        liste_dominos.append(Domino(6, i))
    
    >>> liste_dominos[1].affiche_points()
    face A: 6  face B: 1
    
    etc., etc.
    
  2. Définissez une classe CompteBancaire(), qui permette d'instancier des objets tels que compte1, compte2, etc. Le constructeur de cette classe initialisera deux attributs d'instance "nom" et "solde", avec les valeurs par défaut 'Dupont' et 1000. Trois autres méthodes seront définies :
    • depot(somme) permettra d'ajouter une certaine somme au solde
    • retrait(somme) permettra de retirer une certaine somme du solde
    • affiche() permettra d'afficher le nom du titulaire et le solde de son compte.
    Exemples d'utilisation de cette classe :
    >>> compte1 = CompteBancaire('Duchmol', 800)
    >>> compte1.depot(350)
    >>> compte1.retrait(200)
    >>> compte1.affiche()
    Le solde du compte bancaire de Duchmol est de 950 euros.
    >>> compte2 = CompteBancaire()
    >>> compte2.depot(25)
    >>> compte2.affiche()
    Le solde du compte bancaire de Dupont est de 1025 euros.
    
  3. Définissez une classe Voiture() qui permette d'instancier des objets reproduisant le comportement de voitures automobiles. Le constructeur de cette classe initialisera les attributs d'instance suivants, avec les valeurs par défaut indiquées : marque = 'Ford', couleur = 'rouge', pilote = 'personne', vitesse = 0. Lorsque l'on instanciera un nouvel objet Voiture(), on pourra choisir sa marque et sa couleur, mais pas sa vitesse, ni le nom de son conducteur. Les méthodes suivantes seront définies : - choix_conducteur(nom) permettra de désigner (ou changer) le nom du conducteur - accelerer(taux, duree) permettra de faire varier la vitesse de la voiture. La variation de vitesse obtenue sera égale au produit : taux x duree. Par exemple, si la voiture accélère au taux de 1,3 m/s2 pendant 20 secondes, son gain de vitesse doit être égal à 26 m/s. Des taux négatifs seront acceptés (ce qui permettra de décélérer). La variation de vitesse ne sera pas autorisée si le conducteur est 'personne'. - affiche_tout() permettra de faire apparaître les propriétés présentes de la voiture, c'est-à-dire sa marque, sa couleur, le nom de son conducteur, sa vitesse. Exemples d'utilisation de cette classe :
    >>> a1 = Voiture('Peugeot', 'bleue')
    >>> a2 = Voiture(couleur = 'verte')
    >>> a3 = Voiture('Mercedes')
    >>> a1.choix_conducteur('Roméo')
    >>> a2.choix_conducteur('Juliette')
    >>> a2.accelerer(1.8, 12)
    >>> a3.accelerer(1.9, 11)
    Cette voiture n'a pas de conducteur !
    >>> a2.affiche_tout()
    Ford verte pilotée par Juliette, vitesse = 21.6 m/s.
    >>> a3.affiche_tout()
    Mercedes rouge pilotée par personne, vitesse = 0 m/s.
    
  4. Définissez une classe Satellite() qui permette d'instancier des objets simulant des satellites artificiels lancés dans l'espace, autour de la terre. Le constructeur de cette classe initialisera les attributs d'instance suivants, avec les valeurs par défaut indiquées : masse = 100, vitesse = 0. Lorsque l'on instanciera un nouvel objet Satellite(), on pourra choisir son nom, sa masse et sa vitesse. Les méthodes suivantes seront définies : - impulsion(force, duree) permettra de faire varier la vitesse du satellite. Pour savoir comment, rappelez-vous votre cours de physique : la variation de vitesse subie par un objet de masse m soumis à l'action d'une force F pendant un temps t vaut . Par exemple : un satellite de 300 kg qui subit une force de 600 Newtons pendant 10 secondes voit sa vitesse augmenter (ou diminuer) de 20 m/s. - affiche_vitesse() affichera le nom du satellite et sa vitesse courante. - energie() renverra au programme appelant la valeur de l'énergie cinétique du satellite. Rappel : l'énergie cinétique se calcule à l'aide de la formule Exemples d'utilisation de cette classe :
    >>> s1 = Satellite('Zoé', masse =250, vitesse =10)
    >>> s1.impulsion(500, 15)
    >>> s1.affiche_vitesse()
    vitesse du satellite Zoé = 40 m/s.
    >>> print s1.energie()
    200000
    >>> s1.impulsion(500, 15)
    >>> s1.affiche_vitesse()
    vitesse du satellite Zoé = 70 m/s.
    >>> print s1.energie()
    612500
    

Solution

  1. class Domino:
        def __init__(self, pa, pb):
            self.pa, self.pb = pa, pb
             
        def affiche_points(self):
            print "face A :", self.pa,
            print "face B :", self.pb
            
        def valeur(self):
            return self.pa + self.pb
    
    # Programme de test :
    
    d1 = Domino(2,6)
    d2 = Domino(4,3)
    
    d1.affiche_points()
    d2.affiche_points()
    
    print "total des points :", d1.valeur() + d2.valeur() 
    
    liste_dominos = []
    for i in range(7):
        liste_dominos.append(Domino(6, i))
    
    vt =0
    for i in range(7):
        liste_dominos[i].affiche_points()
        vt = vt + liste_dominos[i].valeur()
        
    print "valeur totale des points", vt
    
  2. Réfléchissez !
  3. class Voiture:
        def __init__(self, marque = 'Ford', couleur = 'rouge'):
            self.couleur = couleur
            self.marque = marque
            self.pilote = 'personne'
            self.vitesse = 0
            
        def accelerer(self, taux, duree):
            if self.pilote =='personne':
                print "Cette voiture n'a pas de conducteur !"
            else:    
                self.vitesse = self.vitesse + taux * duree
            
        def choix_conducteur(self, nom):
            self.pilote = nom    
            
        def affiche_tout(self):
                print "%s %s pilotée par %s, vitesse = %s m/s" % \
                (self.marque, self.couleur, self.pilote, self.vitesse)     
        
    a1 = Voiture('Peugeot', 'bleue')
    a2 = Voiture(couleur = 'verte')
    a3 = Voiture('Mercedes')
    a1.choix_conducteur('Roméo')
    a2.choix_conducteur('Juliette')
    a2.accelerer(1.8, 12)
    a3.accelerer(1.9, 11)
    a2.affiche_tout()
    a3.affiche_tout()
    
  4. class Satellite:
        def __init__(self, nom, masse =100, vitesse =0):
            self.nom, self.masse, self.vitesse = nom, masse, vitesse
             
        def impulsion(self, force, duree):
            self.vitesse = self.vitesse + force * duree / self.masse
            
        def energie(self):
            return self.masse * self.vitesse**2 / 2    
                    
        def affiche_vitesse(self):
            print "Vitesse du satellite %s = %s m/s" \
                              % (self.nom, self.vitesse)
    
    # Programme de test :
    
    s1 = Satellite('Zoé', masse =250, vitesse =10)
    
    s1.impulsion(500, 15)
    s1.affiche_vitesse()
    print s1.energie()
    s1.impulsion(500, 15)
    s1.affiche_vitesse()
    print s1.energie()
    

Exercices

  1. Définissez une classe Cercle(). Les objets construits à partir de cette classe seront des cercles de tailles variées. En plus de la méthode constructeur (qui utilisera donc un paramètre rayon), vous définirez une méthode surface(), qui devra renvoyer la surface du cercle. Définissez ensuite une classe Cylindre() dérivée de la précédente. Le constructeur de cette nouvelle classe comportera les deux paramètres rayon et hauteur. Vous y ajouterez une méthode volume() qui devra renvoyer le volume du cylindre. (Rappel : Volume d'un cylindre = surface de section x hauteur). Exemple d'utilisation de cette classe :
    >>> cyl = Cylindre(5, 7)
    >>> print cyl.surface()
    78.54
    >>> print cyl.volume()
    549.78
    
  2. Complétez l'exercice précédent en lui ajoutant encore une classe Cone(), qui devra dériver cette fois de la classe Cylindre(), et dont le constructeur comportera lui aussi les deux paramètres rayon et hauteur. Cette nouvelle classe possédera sa propre méthode volume(), laquelle devra renvoyer le volume du cône. (Rappel : Volume d'un cône = volume du cylindre correspondant divisé par 3). Exemple d'utilisation de cette classe :
    >>> co = Cone(5,7)
    >>> print co.volume()
    
    183.26
  3. Définissez une classe JeuDeCartes() permettant d'instancier des objets « jeu de cartes » dont le comportement soit similaire à celui d'un vrai jeu de cartes. La classe devra comporter au moins les trois méthodes suivantes : - méthode constructeur : création et remplissage d'une liste de 52 éléments, qui sont eux-mêmes des tuples de 2 éléments contenant les caractéristiques de chacune des 52 cartes. Pour chacune d'elles, il faut en effet mémoriser séparément un nombre entier indiquant la valeur (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, les 4 dernières valeurs étant celles des valet, dame, roi et as), et un autre nombre entier indiquant la couleur de la carte (c'est-à-dire 0,1,2,3 pour Cœur, Carreau, Trèfle & Pique). Dans une telle liste, l'élément (11,2) désigne donc le valet de Trèfle, et la liste terminée doit être du type : [(2, 0), (3,0), (3,0), (4,0), ... ... (12,3), (13,3), (14,3)] - méthode nom_carte() : cette méthode renvoie sous la forme d'une chaîne l'identité d'une carte quelconque, dont on lui a fourni le tuple descripteur en argument. Par exemple, l'instruction : print jeu.nom_carte((14, 3)) doit provoquer l'affichage de : As de pique - méthode battre() : comme chacun sait, battre les cartes consiste à les mélanger. Cette méthode sert donc à mélanger les éléments de la liste contenant les cartes, quel qu'en soit le nombre. - méthode tirer() : lorsque cette méthode est invoquée, une carte est retirée du jeu. Le tuple contenant sa valeur et sa couleur est renvoyé au programme appelant. On retire toujours la première carte de la liste. Si cette méthode est invoquée alors qu'il ne reste plus aucune carte dans la liste, il faut alors renvoyer l'objet spécial None au programme appelant. Exemple d'utilisation de la classe JeuDeCartes() :
    jeu = JeuDeCartes()            # instanciation d'un objet
    jeu.battre()                   # mélange des cartes
    for n in range(53):            # tirage des 52 cartes : 
       c = jeu.tirer() 
       if c == None:               # il ne reste plus aucune carte
          print 'Terminé !'        # dans la liste
       else:
          print jeu.nom_carte(c)   # valeur et couleur de la carte
    
  4. Complément de l'exercice précédent : Définir deux joueurs A et B. Instancier deux jeux de cartes (un pour chaque joueur) et les mélanger. Ensuite, à l'aide d'une boucle, tirer 52 fois une carte de chacun des deux jeux et comparer leurs valeurs. Si c'est la première des 2 qui a la valeur la plus élevée, on ajoute un point au joueur A. Si la situation contraire se présente, on ajoute un point au joueur B. Si les deux valeurs sont égales, on passe au tirage suivant. Au terme de la boucle, comparer les comptes de A et B pour déterminer le gagnant.

Solution

  1. Voir ci-dessous.
  2. #(classes de cylindres et de cônes) :
    # Classes dérivées - polymorphisme
    
    class Cercle:
        def __init__(self, rayon):
            self.rayon = rayon
    
        def surface(self):
            return 3.1416 * self.rayon**2
            
    class Cylindre(Cercle):
        def __init__(self, rayon, hauteur):
            Cercle.__init__(self, rayon)
            self.hauteur = hauteur
            
        def volume(self):
            return self.surface()*self.hauteur
            
            # la méthode surface() est héritée de la classe parente
            
    class Cone(Cylindre):
        def __init__(self, rayon, hauteur):
            Cylindre.__init__(self, rayon, hauteur)
                    
        def volume(self):
            return Cylindre.volume(self)/3
            
            # cette nouvelle méthode volume() remplace celle que
            # l'on a héritée de la classe parente (exemple de polymorphisme)
                                
    cyl = Cylindre(5, 7)
    print cyl.surface()
    print cyl.volume()
    
    co = Cone(5,7)
    print co.surface()
    print co.volume()
    
  3. # Tirage de cartes
    
    from random import randrange
    
    class JeuDeCartes:
        """Jeu de cartes"""
        # attributs de classe (communs à toutes les instances) :
        couleur = ('Pique', 'Trèfle', 'Carreau', 'Cœur')
        valeur = (0, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'valet', 'dame', 'roi', 'as')
    
        def __init__(self):
            "Construction de la liste des 52 cartes"
            self.carte =[]          
            for coul in range(4):
                for val in range(13):
                    self.carte.append((val +2, coul))   # la valeur commence à 2
    
        def nom_carte(self, c):
            "Renvoi du nom de la carte c, en clair"
            return "%s de %s" % (self.valeur[c[0]], self.couleur[c[1]])
            
        def battre(self):
            "Mélange des cartes"
            t = len(self.carte)             # nombre de cartes restantes
            # pour mélanger, on procède à un nombre d'échanges équivalent :
            for i in range(t):
                # tirage au hasard de 2 emplacements dans la liste :         
                h1, h2 = randrange(t), randrange(t)     
                # échange des cartes situées à ces emplacements :
                self.carte[h1], self.carte[h2] = self.carte[h2], self.carte[h1]
            
        def tirer(self):
            "Tirage de la première carte de la pile"
            t = len(self.carte)             # vérifier qu'il reste des cartes 
            if t >0:                        
                carte = self.carte[0]       # choisir la première carte du jeu
                del(self.carte[0])          # la retirer du jeu
                return carte                # en renvoyer copie au prog. appelant
            else:
                return None                 # facultatif
    
    
    ### Programme test :
    
    if __name__ == '__main__':
        jeu = JeuDeCartes()                 # instanciation d'un objet
        jeu.battre()                        # mélange des cartes
        for n in range(53):                 # tirage des 52 cartes : 
            c = jeu.tirer()  		 
            if c == None:                   # il ne reste aucune carte
                print 'Terminé !'           # dans la liste
            else:
                print jeu.nom_carte(c)      # valeur et couleur de la carte
    
  4. #(On supposera que l'exercice précédent a été sauvegardé sous le nom cartes.py)
    # Bataille de cartes
    
    from cartes import JeuDeCartes
    
    jeuA = JeuDeCartes()        # instanciation du premier jeu      
    jeuB = JeuDeCartes()        # instanciation du second jeu      
    jeuA.battre()               # mélange de chacun
    jeuB.battre()
    pA, pB = 0, 0               # compteurs de points des joueurs A et B
    
    # tirer 52 fois une carte de chaque jeu :
    for n in range(52):         
        cA, cB = jeuA.tirer(), jeuB.tirer()
        vA, vB = cA[0], cB[0]   # valeurs de ces cartes
        if vA > vB:
            pA += 1
        elif vB > vA:
            pB += 1             # (rien ne se passe si vA = vB)
        # affichage des points successifs et des cartes tirées :
        print "%s * %s ==> %s * %s" % (jeuA.nom_carte(cA), jeuB.nom_carte(cB), pA, pB) 
    
    print "le joueur A obtient %s points, le joueur B en obtient %s." % (pA, pB)
    


Exercices

Créer vous-même un nouveau module de classes, en encodant les lignes d'instruction ci-dessous dans un fichier que vous nommerez formes.py :

class Rectangle:
    "Classe de rectangles"
    def __init__(self, longueur = 30, largeur = 15):
        self.L = longueur
        self.l = largeur
        self.nom ="rectangle"

    def perimetre(self):
        return "(%s + %s) * 2 = %s" % (self.L, self.l, 
                                            (self.L + self.l)*2)
    def surface(self):
        return "%s * %s = %s" % (self.L, self.l, self.L*self.l)

    def mesures(self):
        print "Un %s de %s sur %s" % (self.nom, self.L, self.l)
        print "a une surface de %s" % (self.surface(),)
        print "et un périmètre de %s\n" % (self.perimetre(),)

class Carre(Rectangle):
    "Classe de carrés"
    def __init__(self, cote =10):
        Rectangle.__init__(self, cote, cote)
        self.nom ="carré"

if __name__ == "__main__":
    r1 = Rectangle(15, 30)
    r1.mesures()    
    c1 = Carre(13)
    c1.mesures()

Une fois ce module enregistré, vous pouvez l'utiliser de deux manières : soit vous en lancez l'exécution comme celle d'un programme ordinaire, soit vous l'importez dans un script quelconque ou depuis la ligne de commande, pour en utiliser les classes :

>>> import formes
>>> f1 = formes.Rectangle(27, 12)
>>> f1.mesures()
Un rectangle de 27 sur 12
a une surface de 27 * 12 = 324
et un périmètre de (27 + 12) * 2 = 78

>>> f2 = formes.Carre(13)
>>> f2.mesures()
Un carré de 13 sur 13
a une surface de 13 * 13 = 169
et un périmètre de (13 + 13) * 2 = 52

On voit dans ce script que la classe Carre() est construite par dérivation à partir de la classe Rectangle() dont elle hérite toutes les caractéristiques. En d'autres termes, la classe Carre() est une classe fille de la classe Rectangle().

Vous pouvez remarquer encore une fois que le constructeur de la classe Carre() fait appel au constructeur de sa classe parente ( Rectangle.__init__() ), en lui transmettant la référence de l'instance (c'est-à-dire self) comme premier argument.

Quant à l'instruction :

if __name__ == "__main__":

placée à la fin du module, elle sert à déterminer si le module est « lancé » en tant que programme (auquel cas les instructions qui suivent doivent être exécutées), ou au contraire utilisé comme une bibliothèque de classes importée ailleurs. Dans ce cas cette partie du code est sans effet.


Solution