9. Classes

Le mécanisme des classes Python ajoute au langage la notion de classes avec un minimum de syntaxe et de sémantique nouvelles. C’est un mélange des mécanismes rencontrés dans C++ et Modula-3. De la même manière que pour les modules, les classes Python ne posent pas de barrière rigide entre leur définition et l’utilisateur, mais s’appuient sur le respect de l’utilisateur à ne pas causer d’effraction dans la définition. Cependant, les fonctionnalités les plus importantes des classes sont conservées avec toutes leur puissance : le mécanisme d’héritage autorise d’avoir plusieurs classes de base, une classe dérivée peut surcharger toutes les méthodes de sa (ou ses) classe(s) de base et une méthode peut faire appel à la méthode d’une classe de base portant le même nom. Les objets peuvent contenir un nombre arbitraire de données.

Dans la terminologie C++, les membres des classes (y compris les données) sont publics (sauf exception, voir Variables privées) et toutes les fonctions membres sont virtuelles. Comme avec Modulo-3, il n’y a aucune façon d’accéder aux membres d’un objet à partir de ses méthodes : une méthode est déclarée avec un premier argument explicite représentant l’objet, et cet argument est transmis de manière implicite lors de l’appel. Comme avec Smalltalk, les classes elles-mêmes sont des objets. Il existe ainsi une sémantique pour les importer et les renommer. Au contraire de C++ et Modulo-3, les types de base peuvent être utilisés comme classes de base pour que l’utilisateur puisse les étendre. Enfin, comme en C++, la plupart des opérateurs de base avec une syntaxe spéciale (opérateurs arithmétiques, sous-indiçage, etc.) peuvent être redéfinis pour les instances de classes.

(Par manque d’une terminologie universellement acceptée pour parler des classes, nous ferons un usage occasionnel des termes de Smalltalk et C++. Nous voulions utiliser les termes de Modula-3 puisque sa sémantique orientée objet est plus proche de celle de Python que de C++, mais il est probable que seul un petit nombre de lecteurs soit susceptibles de les connaître.)

9.1. Quelques mots au sujet des noms et objets

Les objets possèdent une existence propre et plusieurs noms peuvent être utilisés (dans divers contextes) pour faire référence au même objet. Ceux-ci sont connus sous le nom d’alias dans d’autres langages. Ceci est habituellement peu apprécié lors d’un premier coup d’œil à Python et peut être ignoré lorsqu’on travaille avec des types de base non mutables (nombres, chaînes, tuples). Cependant, les alias ont éventuellement des effets surprenants sur la sémantique d’un code Python mettant en jeu des objets mutables comme les listes, les dictionnaires et la plupart des autres types. C’est généralement utilisé au bénéfice du programme car les alias se comportent, d’un certain point de vue, comme des pointeurs. Par exemple, transmettre un objet n’a aucun coût car c’est simplement un pointeur qui est transmis par l’implémentation ; et si une fonction modifie un objet passé en argument, le code à l’origine de l’appel verra le changement. Ceci élimine le besoin d’avoir deux mécanismes de transmission d’arguments comme en Pascal.

9.2. Portées et espaces de noms en Python

Avant de présenter les classes, nous devons parler un peu de la notion de portée en Python. Les définitions de classes font d’habiles manipulations avec les espaces de noms, et vous devez savoir comment les portées et les espaces de noms fonctionnent. Soit dit en passant, toute connaissance sur ce sujet est aussi utile aux développeurs Python expérimentés.

Tout d’abord, quelques définitions.

Un espace de nom est une table de correspondance entre des noms et des objets. La plupart des espaces de noms sont actuellement implémentés sous forme de dictionnaires Python, mais ceci n’est normalement pas visible (sauf pour les performances) et peut changer dans le futur. Comme exemples d’espaces de noms, nous pouvons citer les primitives (fonctions comme abs(), et les noms des exceptions de base) ; les noms globaux dans un module ; et les noms locaux lors d’un appel de fonction. D’une certaine manière, l’ensemble des attributs d’un objet forme lui-même un espace de noms. La chose importante à retenir à propos des espaces de noms est qu’il n’y a absolument aucun lien entre les noms de plusieurs espaces de noms ; par exemple, deux modules différents peuvent définir une fonction maximize sans qu’il y ait de confusion. Les utilisateurs des modules doivent préfixer le nom de la fonction avec celui du module.

À ce propos, nous utilisons le mot attribut pour tout nom suivant un point. Par exemple, dans l’expression z.real, real est un attribut de l’objet z. Rigoureusement parlant, les références à des noms dans des modules sont des références d’attributs : dans l’expression modname.funcname, modname est un objet module et funcname est un attribut de cet objet. Dans ces conditions, il existe une correspondance directe entre les attributs du module et les noms globaux définis dans le module : ils partagent le même espace de noms ! [1]

Les attributs peuvent être seulement lisibles ou aussi modifiables. S’ils sont modifiables, l’affectation à un attribut est possible. Les attributs de modules sont modifiables : vous pouvez écrire modname.the_answer = 42. Les attributs modifiables peuvent aussi être effacés avec l’instruction del. Par exemple, del modname.the_answer supprime l’attribut the_answer de l’objet nommé modname.

Les espaces de noms sont créés à différents moments et ont différentes durées de vie. L’espace de noms contenant les primitives est créé au démarrage de l’interpréteur Python et n’est jamais effacé. L’espace de nom global pour un module est créé lorsque la définition du module est lue. Habituellement, les espaces de noms des modules durent aussi jusqu’à l’arrêt de l’interpréteur. Les instructions exécutées par la première invocation de l’interpréteur, qu’ils soient lus depuis un fichier de script ou de manière interactive, sont considérés comme faisant partie d’un module appelé __main__, de façon qu’elles possèdent leur propre espace de noms. (les primitives vivent elles-mêmes dans un module, appelé builtins.)

L’espace de noms local d’une fonction est créé lors de son appel, puis effacé lorsqu’elle renvoie un résultat ou lève une exception non prise en charge. (En fait, « oublié » serait une meilleure façon de décrire ce qui se passe réellement). Bien sûr, des invocations récursives ont chacune leur propre espace de noms.

Une portée est une zone textuelle d’un programme Python où un espace de noms est directement accessible. « Directement accessible » signifie ici qu’une référence non qualifée à un nom sera recherchée dans l’espace de noms.

Bien que les portées soient déterminées de manière statique, elles sont utilisées de manière dynamique. À n’importe quel moment de l’exécution, il y a au minimum trois portées imbriquées dont les espaces de noms sont directement accessibles :

  • La portée la plus au centre, celle qui est consultée en premier, contient les noms locaux
  • les portées des fonctions englobantes, qui sont consultées en commençant avec la portée englobante la plus proche, contiennent des noms non-locaux mais aussi non-globaux
  • l’avant dernière portée contient les noms globaux du module courant
  • la portée englobante, consultée en dernier, est l’espace de noms contenant les primitives

If a name is declared global, then all references and assignments go directly to the middle scope containing the module’s global names. To rebind variables found outside of the innermost scope, the nonlocal statement can be used; if not declared nonlocal, those variable are read-only (an attempt to write to such a variable will simply create a new local variable in the innermost scope, leaving the identically named outer variable unchanged).

Habituellement, la portée locale référence les noms locaux de la fonction courante. En dehors des fonctions, la portée locale référence le même espace de noms que la portée globale : l’espace de noms du module. Les définitions de classes créent un nouvel espace de noms dans la portée locale.

Il est important de réaliser que les portées sont déterminées de manière textuelle : la portée globale d’une fonction définie dans un module est l’espace de nom de ce module, quel que soit la provenance de cet appel. Par contre, la recherche réelle des noms est faite dynamiquement au moment de l’exécution. Cependant la définition du langage est en train d’évoluer vers une résolution statique des noms au moment de la « compilation », donc sans se baser sur une résolution dynamique ! (En réalité, les variables locales sont déjà déterminées de manière statique).

Une particularité de Python est que si aucune instruction global n’est active, les affectations de noms vont toujours dans la portée la plus proche. Les affectations ne copient aucune donnée : elles se contentent de lier des noms à des objets. Ceci est également vrai pour l’effacement : l’instruction del x supprime la liaison de x dans l’espace de noms référencé par la portée locale. En réalité, toutes les opérations qui impliquent des nouveaux noms utilisent la portée locale : en particulier, les instructions import et les définitions de fonctions effectuent une liaison du module ou du nom de fonction dans la portée locale.

L’instruction global peut être utilisée pour indiquer que certaines variables existent dans la portée globale et doivent être reliées en local ; l’instruction nonlocal indique que certaines variables existent dans une portée supérieure et doivent être reliées en local.

9.2.1. Exemple de portées et d’espaces de noms

Ceci est un exemple montrant comment utiliser les différentes portées et espaces de noms, et comment global et nonlocal modifient l’affectation de variable :

def scope_test():
    def do_local():
        spam = "local spam"
    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"
    def do_global():
        global spam
        spam = "global spam"
    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

Ce code donne le résultat suivant :

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

Vous pouvez constater que l’affectation locale (qui est effectuée par défaut) n’a pas modifié la liaison de spam dans scope_test. L’affectation nonlocal a changé la liaison de spam dans scope_test et l’affectation global a changé la liaison au niveau du module.

Vous pouvez également voir qu’aucune liaison pour spam n’a été faite avant l’affectation global.

9.3. Une première approche des classes

Le concept de classes introduit quelques nouveau éléments de syntaxe, trois nouveaux types d’objets ainsi que de nouveaux éléments de sémantique

9.3.1. Syntaxe de définition des classes

La forme la plus simple de définition de classe ressemble à ceci :

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

Les définitions de classes, comme les définitions de fonctions (définitions def) doivent être exécutées avant d’avoir un effet. (Vous pouvez tout à fait placer une définition de classe dans une branche d’une instruction conditionnelle if ou encore à l’intérieur d’une fonction.)

Dans la pratique, les déclarations dans une définition de classe seront généralement des définitions de fonctions, mais d’autres déclarations sont permises, et parfois utiles — Nous reviendrons sur ce point plus tard. Les définitions de fonction à l’intérieur d’une classe ont normalement une forme particulière de liste d’arguments, dictée par les conventions d’appel aux méthodes — À nouveau, tout ceci sera expliqué plus tard.

Quand une classe est définie, un nouvel espace de noms est créé et utilisé comme portée locale — Ainsi, toutes les affectations de variables locales entrent dans ce nouvel espace de noms. En particulier, les définitions de fonctions y lient le nom de la nouvelle fonction.

A la fin de la définition d’une classe, un objet classe est créé. C’est, pour simplifier, une encapsulation du contenu de l’espace de noms créé par la définition de classe. Nous reparlerons des objets classes dans la prochaine section. La portée locale initiale (celle qui prévaut avant le début de la définition de la classe) est réinstanciée, et l’objet de classe est lié ici au nom de classe donné dans l’en-tête de définition de classe (NomDeLaClasse dans l’exemple).

9.3.2. Les objets classe

Les objets classes prennent en charge deux types d’opérations : des références à des attributs et l’instanciation.

Les références d’attributs utilisent la syntaxe standard utilisée pour toutes les références d’attributs en Python : obj.nom. Les noms d’attribut valides sont tous les noms qui se trouvaient dans l’espace de noms de la classe quand l’objet classe a été créé. Donc, si la définition de classe ressemble à ceci :

class MyClass:
    """A simple example class"""
    i = 12345
    def f(self):
        return 'hello world'

alors MaClasse.i et MaClasse.f sont des références valides à des attributs, renvoyant respectivement un entier et un objet fonction. Les attributs de classes peuvent également être affectés, de sorte que vous pouvez modifier la valeur de MaClasse.i par affectation. __doc__ est aussi un attribut valide, renvoyant la docstring appartenant à la classe : "Une simple classe d'exemple".

L”instanciation de classes utilise la notation des fonctions. Considérez simplement que l’objet classe est une fonction sans paramètre qui renvoie une nouvelle instance de la classe. Par exemple (en considérant la classe définie ci-dessus)

x = MyClass()

crée une nouvelle instance de la classe et affecte cet objet à la variable locale x.

L’opération d’instanciation (en « appelant » un objet classe) crée un objet vide. De nombreuses classes aiment créer des objets personnalisés avec des instances personnalisées en fonction d’un état initial spécifique. Ainsi une classe peut définir une méthode spéciale nommée: __init__(), comme ceci :

def __init__(self):
    self.data = []

Quand une classe définit une méthode __init__(), l’instanciation de la classe appelle automatiquement __init__() pour la nouvelle instance de la classe. Donc, dans cet exemple, l’initialisation d’une nouvelle instance peut être obtenue par :

x = MyClass()

Bien sûr, la méthode __init__() peut avoir des arguments pour une plus grande flexibilité. Dans ce cas, les arguments donnés à l’opérateur d’instanciation de classe sont transmis à __init__(). Par exemple,

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

9.3.3. Objets instance

Maintenant, que pouvons-nous faire avec des objets instance ? Les seules opérations comprises par les objets instances sont des références d’attributs. Il y a deux sortes de noms d’attributs valides, les attributs données et les méthodes.

Les attributs données correspondent à des « variables d’instance » en Smalltalk, et aux « membres de données » en C++. Les attributs données n’ont pas à être déclarés. Comme les variables locales, ils existent dès lors qu’ils sont attribués une première fois. Par exemple, si x est l’instance de MyClass créée ci-dessus, le code suivant affiche la valeur 16, sans laisser de traces :

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

L’autre type de référence à un attribut d’instance est une méthode. Une méthode est une fonction qui « appartient à » un objet (en Python, le terme de méthode n’est pas unique aux instances de classes : d’autres types d’objets peuvent aussi avoir des méthodes. Par exemple, les objets listes ont des méthodes appelées append, insert, remove, sort, et ainsi de suite. Toutefois, dans la discussion qui suit, sauf indication contraire, nous allons utiliser le terme de méthode exclusivement en référence à des méthodes d’objets instances de classe).

Les noms de méthodes valides d’un objet instance dépendent de sa classe. Par définition, tous les attributs d’une classe qui sont des objets fonction définissent les méthodes correspondantes de ses instances. Donc, dans notre exemple, x.f est une méthode de référence valide, car MaClasse.f est une fonction, mais pas x.i car MaClasse.i n’en est pas une. Attention cependant, x.f n’est pas la même chose que MaClasse.f — Il s’agit d’un objet méthode, pas d’un objet fonction.

9.3.4. Les objets méthode

Le plus souvent, une méthode est appelée juste après avoir été liée:

x.f()

Dans l’exemple de la class MaClass, cela va retourner la chaîne de caractères hello world. Toutefois, il n’est pas nécessaire d’appeler la méthode directement: x.f est un objet méthode, il peut être gardé de coté et être appelé plus tard. Par exemple:

xf = x.f
while True:
    print(xf())

va afficher hello world jusqu’à la fin des temps.

Que ce passe-t-il exactement quand une méthode est appelée ? Vous avez dû remarquer que x.f() a été appelée dans le code ci-dessus sans argument, alors que la définition de la méthode f() spécifie bien qu’elle prend un argument. Qu’est-il arrivé à l’argument ? Python doit sûrement lever une exception lorsqu’une fonction qui requiert un argument est appelée sans – même si l’argument n’est pas utilisé…

Actually, you may have guessed the answer: the special thing about methods is that the object is passed as the first argument of the function. In our example, the call x.f() is exactly equivalent to MyClass.f(x). In general, calling a method with a list of n arguments is equivalent to calling the corresponding function with an argument list that is created by inserting the method’s object before the first argument.

Si vous ne comprenez toujours pas comment les méthodes fonctionnent, un coup d’œil à l’implémentation vous aidera peut être. Lorsque l’instance d’un attribut est référencé qui n’est pas un attribut donnée, sa classe est recherchée. Si le nom correspond à un attribut valide qui est une fonction, un objet méthode est créé en associant (via leurs pointeurs) l’objet instance et l’objet fonction trouvé ensemble dans un nouvel objet abstrait : c’est l’objet méthode. Lorsque l’objet méthode est appelé avec une liste d’arguments, une nouvelle liste d’arguments est construite à partir de l’objet méthode et de la liste des arguments. L’objet fonction est appelé avec cette nouvelle liste d’arguments.

9.3.5. Classes et variables d’instance

En général, les variables d’instance stockent des informations relatives à chaque instance alors que les variables de classe servent à stocker les attributs et méthodes communes à toutes les instances de la classe:

class Dog:

    kind = 'canine'         # class variable shared by all instances

    def __init__(self, name):
        self.name = name    # instance variable unique to each instance

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

Comme vu dans Quelques mots au sujet des noms et objets, les données partagées modifiables (tel que les listes, dictionnaires, etc…) peuvent avoir des effets surprenants. Part exemple, la liste tricks dans le code suivant ne devrait pas être une variable de classe, car jiate une seule liste serait partagées par toutes les instances de Dog:

class Dog:

    tricks = []             # mistaken use of a class variable

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

Une conception correcte de la classe serait d’utiliser une variable d’instance à la place : :

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

9.4. Remarques diverses

Les attributs de données surchargent les méthodes avec le même nom ; pour éviter des conflits de nommage, qui peuvent causer des bugs difficiles à trouver dans de grands programmes, il est sage d’adopter certaines conventions qui minimisent les risques de conflits. Les conventions possibles comprennent la mise en majuscule des noms de méthodes, le préfixe des noms d’attributs de données par une chaîne courte et unique (parfois juste la caractère souligné), ou l’utilisation de verbes pour les méthodes et de noms pour les attributs de données.

Les attributs de données peuvent être référencés par des méthodes comme par des utilisateurs ordinaires (« clients ») d’un objet. En d’autres termes, les classes ne sont pas utilisables pour implémenter des types de données purement abstraits. En fait, rien en Python ne rend possible d’imposer de masquer des données — tout est basé sur des conventions (d’un autre coté, l’implémentation Python, écrite en C, peut complètement masquer les détails d’implémentation et contrôler l’accès à un objet si nécessaire ; ceci peut être utilisé par des extensions de Python écrites en C).

Les clients doivent utiliser les attributs de données avec précaution — ils pourraient mettre le désordre dans les invariants gérés par les méthodes avec leurs propres valeurs d’attributs. Remarquez que les clients peuvent ajouter leurs propres attributs de données à une instance d’objet sans altérer la validité des méthodes, pour autant que les noms n’entrent pas en conflit — aussi, adopter une convention de nommage peut éviter bien des problèmes.

Il n’y a pas de notation abrégée pour référencer des attributs de données (ou d’autres méthodes !) depuis les méthodes. Nous pensons que ceci améliore en fait la lisibilité des méthodes : il n’y a aucune chance de confondre variables locales et variables d’instances quand on regarde le code d’une méthode.

Souvent, le premier argument d’une méthode est nommé self. Ce n’est qu’une convention : le nom self n’a aucune signification particulière en Python. Notez cependant que si vous ne suivez pas cette convention, votre code risque d’être moins lisible pour d’autres programmeurs Python, et il est aussi possible qu’un programme qui fasse l’introspection de classes repose sur une telle convention.

Tout objet fonction qui est un attribut de classe définit une méthode pour des instances de cette classe. Il n’est pas nécessaire que le texte de définition de la fonction soit dans la définition de la classe : il est possible d’affecter un objet fonction à une variable locale de la classe. Par exemple :

# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1
    def g(self):
        return 'hello world'
    h = g

Maintenant, f, g et h sont tous des attributs des classes C faisant référence aux fonctions objets, et par conséquent sont toutes des méthodes des instances de Ch est exactement identique à g. Remarquez qu’en pratique, ceci ne sert qu’à embrouiller le lecteur d’un programme.

Les méthodes peuvent appeler d’autres méthodes en utilisant des méthodes qui sont des attributs de l’argument self

class Bag:
    def __init__(self):
        self.data = []
    def add(self, x):
        self.data.append(x)
    def addtwice(self, x):
        self.add(x)
        self.add(x)

Les méthodes peuvent faire référence à des noms globaux de la même manière que les fonctions. La portée globale associée à une méthode est le module contenant la définition de la classe (la classe elle même n’est jamais utilisée en tant que portée globale). Alors qu’on rencontre rarement une bonne raison d’utiliser des données globales dans une méthode, il y a de nombreuses utilisations légitimes d’une portée globale : par exemple, les fonctions et modules importés dans une portée globale peuvent être utilisés par des méthodes, de même que les fonctions et classes définies dans cette même portée. Habituellement, la classe contenant la méthode est elle même définie dans cette portée globale, et dans la section suivante, nous verrons de bonnes raisons pour qu’une méthode référence sa propre classe.

Toute valeur est un objet, et a donc une classe (appelé aussi son type). Elle est stockée dans objet.__class__.

9.5. L’héritage

Bien sûr, ce terme de «classe» ne serait pas utilisé s’il n’y avait pas l’héritage. La syntaxe pour définir une sous-classe ressemble à ceci :

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

Le nom ClasseDeBase doit être défini dans un espace contenant la définition de la classe dérivée. A la place du nom d’une classe de base, une expression est aussi autorisée. Ceci peut être utile, par exemple, lorsque la classe est définie dans un autre module :

class DerivedClassName(modname.BaseClassName):

L’exécution d’une définition de classe dérivée se déroule comme pour une classe de base. Quand l’objet de la classe est construit, la classe de base est mémorisée. Elle est utilisée pour la résolution des références d’attributs : si un attribut n’est pas trouvé dans la classe, la recherche procède en regardant dans la classe de base. Cette règle est appliquée récursivement si la classe de base est elle-même dérivée d’une autre classe.

Il n’y a rien de particulier dans l’instantiation des classes dérivées : DerivedClassName() crée une nouvelle instance de la classe. Les références aux méthodes sont résolues comme suit : l’attribut correspondant de la classe est recherché, en remontant la hiérarchie des classes de base si nécessaire, et la référence de méthode est valide si cela conduit à une fonction.

Les classes dérivées peuvent surcharger des méthodes de leurs classes de base. Comme les méthodes n’ont aucun privilège particulier quand elles appellent d’autres méthodes d’un même objet, une méthode d’une classe de base qui appelle une autre méthode définie dans la même classe peut en fait appeler une méthode d’une classe dérivée qui la surcharge (pour les programmeurs C++ : toutes les méthodes de Python sont en effet virtual).

Une méthode surchargée dans une classe dérivée peut en fait vouloir étendre plutôt que simplement remplacer la méthode du même nom de sa classe de base. Il y a une façon simple d’appeler la méthode de la classe de base directement : appelez simplement BaseClassName.methodname(self, arguments). Ceci est parfois utile également aux clients (notez bien que ceci ne fonctionne que si la classe de base est accessible en tant que ClasseDeBase dans la portée globale).

Python a deux fonctions primitives qui gèrent l’héritage :

  • Utilisez isinstance() pour tester le type d’une instance : isinstance(obj, int) renverra True seulement si obj.__class__ est égal à int ou à une autre classe dérivée de int.
  • Utilisez issubclass() pour tester l’héritage d’une class : issubclass(bool, int) renvoie True car la class bool est une sous-classe de int. Par contre, issubclass(float, int) renvoie False car float n’est pas une sous-classe de int.

9.5.1. L’héritage multiple

Python gère également une forme d’héritage multiple. Une définition de classe ayant plusieurs classes de base ressemble à :

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

Dans la plupart des cas, vous pouvez imaginer la recherche d’attributs dans les classes parentes comme étant : le plus profond d’abord, de gauche à droite, sans chercher deux fois dans la même classe si elle apparaît plusieurs fois dans la hiérarchie. Ainsi, si un attribut n’est pas trouvé dans DerivedClassName, il est recherché dans Base1, puis (récursivement) dans les classes de base de Base1 ; s’il n’y est pas trouvé, il est recherché dans Base2 et ses classes de base, et ainsi de suite.

Dans les faits, c’est un peu plus complexe que ça, l’ordre de la recherche (method resolution order, ou MRO) change dynamiquement pour gérer des appels coopératifs à super(). Cette approche est connue sous le nom de la « méthode la plus proche » (« call-next-method ») dans d’autres langages supportant l’héritage multiple, et est plus puissante que l’appel à super trouve dans les langages à héritage simple.

L’ordre défini dynamiquement est nécessaire car tous les cas d’héritage multiple comportent une ou plusieurs relations en losange (où au moins une classe peut être accédée à partir de plusieurs chemins en pariant de la classe la plus base). Par exemple, puisque toutes les classes héritent de object, tout héritage multiple ouvre plusieurs chemins pour atteindre object. Pour qu’une classe de base ne soit pas appelée plusieurs fois, l’algorithme dynamique linéarise l’ordre de recherche d’une façon qui préserve l’ordre d’héritage, de lagauche vers la droite, spécifié dans chaque classe, qui appelle chaque classe parente une seule fois, qui est monotone (ce qui signifie qu’une classe peut être sous-classée sans affecter l’ordre d’héritage de ses parents). Prises ensemble, ces propriétés permettent de concevoir des classes de façon fiable et extensible dans un contexte d’héritage multiple. Pour plus de détail, consultez http://www.python.org/download/releases/2.3/mro/.

9.6. Variables privées

Les membres « privés », qui ne peuvent être accédés en dehors d’un objet, n’existent pas en Python. Toutefois, il existe une convention respectée par la majorité du code Python : un nom préfixé par un tiret bas (comme _spam) doit être vu comme une partie non publique de l’API (qu’il s’agisse d’une fonction, d’une méthode ou d’une variable membre). Il doit être considéré comme un détail d’implémentation pouvant faire l’objet de modification futures sans préavis.

Dès lors qu’il y a un cas d’utilisation valable pour avoir des membres privés (notamment pour éviter des conflits avec des noms définis dans des sous-classes), il existe un support (certes limité) pour un tel mécanisme, appelé name mangling. Tout identifiant sous la forme __spam (avec au moins deux underscores en tête, et au plus un à la fin) est remplacé textuellement par _classname__spam, où classname est le nom de la classe sans le(s) premier(s) underscore(s). Ce « bricolage » est effectué sans tenir compte de la position syntaxique de l’identifiant, tant qu’il est présent dans la définition d’une classe.

Ce changement de nom est utile pour permettre à des sous-classes de surcharger des méthodes sans interrompre les appels de méthodes intra-classes. Par exemple :

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

Notez que ces règles sont conçues avant tout pour éviter les accidents ; il reste possible d’accéder ou de modifier une variable considérée comme privée. Ceci peut même être utile dans certaines circonstances, comme au sein du débogueur.

Notez que le code passé à exec(), eval() ne considère pas le nom de la classe appelante comme étant la classe courante ; le même effet s’applique à la directive global, dont l’effet est de la même façon restreint au code compilé dans le même ensemble de byte-code. Les mêmes restrictions s’appliquent à getattr(), setattr() et delattr(), ainsi qu’aux références directes à __dict__.

9.7. Trucs et astuces

Il est parfois utile d’avoir un type de donnée similaire au « record » du Pascal ou au « struct » du C, qui regroupent ensemble quelques attributs nommés. La définition d’une classe vide remplit parfaitement ce besoin :

class Employee:
    pass

john = Employee() # Create an empty employee record

# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

On peut souvent fournir, à du code Python qui s’attend à recevoir un type de donnée abstrait spécifique, une classe qui simule les méthodes de ce type. Par exemple, si vous avez une fonction qui formate des données extraites d’un objet fichier, vous pouvez définir une classe avec des méthodes read() et readline() qui extrait ses données d’un tampon de chaînes de caractères à la place, et lui passer une instance comme argument.

Les objets méthodes d’instances ont également des attributs : m.im_self est l’instance d’objet avec la méthode m(), et m.im_func est l’objet fonction correspondant à la méthode.

9.8. Exceptions Are Classes Too

User-defined exceptions are identified by classes as well. Using this mechanism it is possible to create extensible hierarchies of exceptions.

There are two new valid (semantic) forms for the raise statement:

raise Class

raise Instance

In the first form, Class must be an instance of type or of a class derived from it. The first form is a shorthand for:

raise Class()

A class in an except clause is compatible with an exception if it is the same class or a base class thereof (but not the other way around — an except clause listing a derived class is not compatible with a base class). For example, the following code will print B, C, D in that order:

class B(Exception):
    pass
class C(B):
    pass
class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

Note that if the except clauses were reversed (with except B first), it would have printed B, B, B — the first matching except clause is triggered.

When an error message is printed for an unhandled exception, the exception’s class name is printed, then a colon and a space, and finally the instance converted to a string using the built-in function str().

9.9. Itérateurs

Vous avez maintenant certainement remarqué que l’on peut itérer sur la plupart des objets conteneurs en utilisant une instruction for

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

Ce mode d’accès est simple, concis et pratique. L’utilisation d’itérateurs imprègne et unifie Python. En arrière plan, l’instruction for appelle la fonction iter() sur l’objet conteneur. Cette fonction renvoie un itérateur qui définit la méthode __next__(), laquelle accèdeaux éléments du conteneur un par un. Lorsqu’il n’y a plus d’élément, __next__() lève une exception StopIteration qui indique à la boucle de l’instruction for de se terminer. Vous pouvez appeller la méthode __next__() en utilisant la fonction native next(). Cet exemple montre comment tout cela fonctionne:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
    next(it)
StopIteration

Une fois compris les mécanismes de gestion des itérateurs, il est simple d’ajouter ce comportement à vos classes. Définissez une méthode __iter__(), qui retourne un objet disposant d’une méthode __next__(). Sila classe définit elle-même la méthode __next__(), alors __iter__() peut simplement renvoyer self

class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)
    def __iter__(self):
        return self
    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

9.10. Générateurs

Les générateurs sont des outils simples et puissants pour créer des itérateurs. Ils sont écrits comme des fonctions classiques mais utilisent l’instruction yield lorsqu’ils veulent renvoyer des données. À chaque fois que next() est appelée, le générateur reprend son exécution là où il s’était arrété (en conservant tout son contexte d’exécution). Un exemple montre très bien combien les générateurs sont simples à créer :

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

Tout ce qui peut être fait avec des générateurs peut également être fait avec des itérateurs basés sur des classes, comme décrit dans le paragraphe précédent. Si qui fait que les générateurs sont si compacts est que les méthodes __iter__() et __next__() sont créées automatiquement.

Une autre fonctionnalité clé est que les variables locales ainsi que le contexte d’exécution sont sauvegardés automatiquement entre les appels. Cela simplifie d’autant plus l’écriture de ces fonctions, et rend leur code beaucoup plus lisible qu’avec une approche utilisant des variables d’instance telles que self.index et self.data.

En plus de la création automatique de méthodes et de la sauvegarde du contexte d’exécution, les générateurs lèvent automatiquement une exception StopIteration lorsqu’ils terminent leur exécution. Combinées, ces fonctionnalités rendent très simple la création d’itérateurs sans plus d’effort que l’écriture d’une fonction classique.

9.11. Expressions et générateurs

Des générateurs simples peuvent être codés très rapidement avec des expressions utilisant la même syntaxe que les compréhensions de listes, mais en utilisant des parenthèses à la place des crochets. Ces expressions sont conçues pour des situations où le générateur est utilisé tout de suite dans une fonction. Ces expressions sont plus compactes mais moins souples que des définitions complètes de générateurs, et ont tendance à être plus économes en mémoire que leur équivalent en compréhension de listes.

Exemples :

>>> sum(i*i for i in range(10))                 # sum of squares
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260

>>> from math import pi, sin
>>> sine_table = {x: sin(x*pi/180) for x in range(0, 91)}

>>> unique_words = set(word  for line in page  for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

Notes

[1]Except for one thing. Module objects have a secret read-only attribute called __dict__ which returns the dictionary used to implement the module’s namespace; the name __dict__ is an attribute but not a global name. Obviously, using this violates the abstraction of namespace implementation, and should be restricted to things like post-mortem debuggers.