9. Classes
**********

Les classes sont un moyen de réunir des données et des
fonctionnalités. Créer une nouvelle classe crée un nouveau *type*
d'objet et ainsi de nouvelles *instances* de ce type peuvent être
construites. Chaque instance peut avoir ses propres attributs, ce qui
définit son état. Une instance peut aussi avoir des méthodes (définies
par la classe de l'instance) pour modifier son état.

La notion de classes en Python s'inscrit dans le langage 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. Les classes fournissent
toutes les fonctionnalités standards de la programmation orientée
objet : l'héritage de classes autorise les héritages multiples, une
classe dérivée peut surcharger les méthodes de sa ou ses classes mères
et une méthode peut appeler la méthode d'une classe mère qui possède
le même nom. Les objets peuvent contenir n'importe quel nombre ou type
de données. De la même manière que les modules, les classes
participent à la nature dynamique de Python : elles sont créées
pendant l'exécution et peuvent être modifiées après leur création.

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 Modula-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 Modula-3, les types natifs peuvent
être utilisés comme classes mères pour être étendus par l'utilisateur.
Enfin, comme en C++, la plupart des opérateurs natifs avec une syntaxe
spéciale (opérateurs arithmétiques, indiçage, etc.) peuvent être
redéfinis pour les instances de classes.

En l'absence d'une terminologie communément admise pour parler des
classes, nous utilisons parfois 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 C++, mais il est
probable que seul un petit nombre de lecteurs les connaissent.


9.1. Objets et noms : préambule
===============================

Les objets possèdent une existence propre et plusieurs noms peuvent
être utilisés (dans divers contextes) pour faire référence à un même
objet. Ce concept est connu sous le nom d'alias dans d'autres
langages. Il n'apparaît pas au premier coup d'œil en Python et il peut
être ignoré tant qu'on travaille avec des types de base immuables
(nombres, chaînes, *n*-uplets). Cependant, les alias peuvent produire
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. En général, leur utilisation est bénéfique
au 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 voit 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 nommage en Python
============================================

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

Commençons par quelques définitions.

Un *espace de nommage* est une table de correspondance entre des noms
et des objets. La plupart des espaces de nommage 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 nommage, 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 nommage. L'important à retenir
concernant les espaces de nommage est qu'il n'y a absolument aucun
lien entre les noms de différents espaces de nommage ; par exemple,
deux modules différents peuvent définir une fonction "maximize" sans
qu'il n'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
"nommodule.nomfonction", "nommodule" est un objet module et
"nomfonction" 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
nommage [1] !

Attributes may be read-only or writable.  In the latter case,
assignment to attributes is possible.  Module attributes are writable:
you can write "modname.the_answer = 42".  Writable attributes may also
be deleted with the "del" statement.  For example, "del
modname.the_answer" will remove the attribute "the_answer" from the
object named by "modname".

Les espaces de nommage sont créés à différents moments et ont
différentes durées de vie. L'espace de nommage contenant les
primitives est créé au démarrage de l'interpréteur Python et n'est
jamais effacé. L'espace de nommage globaux pour un module est créé
lorsque la définition du module est lue. Habituellement, les espaces
de nommage 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'elles soient lues depuis un fichier de script ou de
manière interactive, sont considérées comme faisant partie d'un module
appelé "__main__", de façon qu'elles possèdent leur propre espace de
nommage (les primitives vivent elles-mêmes dans un module, appelé
"builtins").

L'espace des noms locaux 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 nommage.

La *portée* est la zone textuelle d'un programme Python où un espace
de nommage est directement accessible. « Directement accessible »
signifie ici qu'une référence non qualifiée à un nom est cherchée dans
l'espace de nommage.

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 ou quatre portées imbriquées dont
les espaces de nommage 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 nommage
  contenant les primitives.

Si un nom est déclaré comme global, alors toutes les références et
affectations vont directement dans l'avant-dernière portée contenant
les noms globaux du module. Pour pointer une variable qui se trouve en
dehors de la portée la plus locale, vous pouvez utiliser l'instruction
"nonlocal". Si une telle variable n'est pas déclarée *nonlocal*, elle
est en lecture seule (toute tentative de la modifier crée simplement
une *nouvelle* variable dans la portée la plus locale, en laissant
inchangée la variable du même nom dans sa portée d'origine).

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 nommage que la portée globale : l'espace de nommage
du module. Les définitions de classes créent un nouvel espace de
nommage 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 nommage de ce module, quelle que soit la
provenance de l'appel à la fonction. En revanche, 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 ne vous basez
pas 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" ou
"nonlocal" 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 nommage 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 nommage
-------------------------------------------------

Ceci est un exemple montrant comment utiliser les différentes portées
et espaces de nommage, 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 classe introduit un peu de syntaxe nouvelle, trois
nouveaux types d'objets ainsi que quelques nouveaux éléments de
sémantique.


9.3.1. Syntaxe de définition des classes
----------------------------------------

La forme la plus simple de définition d'une classe est la suivante :

   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 sont
généralement des définitions de fonctions mais d'autres déclarations
sont permises et parfois utiles (nous revenons 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 est
expliqué plus loin).

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

When a class definition is left normally (via the end), a *class
object* is created.  This is basically a wrapper around the contents
of the namespace created by the class definition; we'll learn more
about class objects in the next section.  The original local scope
(the one in effect just before the class definition was entered) is
reinstated, and the class object is bound here to the class name given
in the class definition header ("ClassName" in the example).


9.3.2. Objets classes
---------------------

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 nommage de la classe quand l'objet classe a été créé. Donc, si la
définition de classe est de cette forme :

   class MyClass:
       """A simple example class"""
       i = 12345

       def f(self):
           return 'hello world'

then "MyClass.i" and "MyClass.f" are valid attribute references,
returning an integer and a function object, respectively. Class
attributes can also be assigned to, so you can change the value of
"MyClass.i" by assignment. "__doc__" is also a valid attribute,
returning the docstring belonging to the class: ""A simple example
class"".

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".

The instantiation operation ("calling" a class object) creates an
empty object. Many classes like to create objects with instances
customized to a specific initial state. Therefore a class may define a
special method named "__init__()", like this:

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

When a class defines an "__init__()" method, class instantiation
automatically invokes "__init__()" for the newly created class
instance.  So in this example, a new, initialized instance can be
obtained by:

   x = MyClass()

Of course, the "__init__()" method may have arguments for greater
flexibility.  In that case, arguments given to the class instantiation
operator are passed on to "__init__()".  For example,

   >>> 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 instances
-----------------------

Maintenant, que pouvons-nous faire avec des objets instances ? 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.

*data attributes* correspond to "instance variables" in Smalltalk, and
to "data members" in C++.  Data attributes need not be declared; like
local variables, they spring into existence when they are first
assigned to.  For example, if "x" is the instance of "MyClass" created
above, the following piece of code will print the value "16", without
leaving a trace:

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

The other kind of instance attribute reference is a *method*. A method
is a function that "belongs to" an object.

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 fonctions définissent les méthodes correspondantes de ses
instances. Donc, dans notre exemple, "x.f" est une référence valide à
une méthode car "MyClass.f" est une fonction, mais pas "x.i" car
"MyClass.i" n'en est pas une. Attention cependant, "x.f" n'est pas la
même chose que "MyClass.f" --- Il s'agit d'un *objet méthode*, pas
d'un objet fonction.


9.3.4. Objets méthode
---------------------

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

   x.f()

In the "MyClass" example, this will return the string "'hello world'".
However, it is not necessary to call a method right away: "x.f" is a
method object, and can be stored away and called at a later time.  For
example:

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

affiche "hello world" jusqu'à la fin des temps.

What exactly happens when a method is called?  You may have noticed
that "x.f()" was called without an argument above, even though the
function definition for "f()" specified an argument.  What happened to
the argument? Surely Python raises an exception when a function that
requires an argument is called without any --- even if the argument
isn't actually used...

En fait, vous avez peut-être deviné la réponse : la particularité des
méthodes est que l'objet est passé comme premier argument de la
fonction. Dans notre exemple, l'appel "x.f()" est exactement
équivalent à "MyClass.f(x)". En général, appeler une méthode avec une
liste de *n* arguments est équivalent à appeler la fonction
correspondante avec une liste d'arguments créée en ajoutant l'instance
de l'objet de la méthode avant le premier argument.

In general, methods work as follows.  When a non-data attribute of an
instance is referenced, the instance's class is searched. If the name
denotes a valid class attribute that is a function object, references
to both the instance object and the function object are packed into a
method object.  When the method object is called with an argument
list, a new argument list is constructed from the instance object and
the argument list, and the function object is called with this new
argument list.


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 nous l'avons vu dans Objets et noms : préambule, les données
partagées *mutable* (telles que les listes, dictionnaires, etc.)
peuvent avoir des effets surprenants. Par exemple, la liste *tricks*
dans le code suivant ne devrait pas être utilisée en tant que variable
de classe car, dans ce cas, une seule liste est partagée 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 est 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
=======================

Si le même nom d'attribut apparaît à la fois dans une instance et dans
une classe, alors la recherche d'attribut donne la priorité à
l'instance :

   >>> class Warehouse:
   ...    purpose = 'storage'
   ...    region = 'west'
   ...
   >>> w1 = Warehouse()
   >>> print(w1.purpose, w1.region)
   storage west
   >>> w2 = Warehouse()
   >>> w2.region = 'east'
   >>> print(w2.purpose, w2.region)
   storage east

Les attributs '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, il n'est pas possible en
Python d'imposer de masquer des données — tout est basé sur des
conventions (d'un autre côté, l'implémentation de 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 '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 'données' à une
instance d'objet sans altérer la validité des méthodes, pour autant
que les noms n'entrent pas en conflit --- là 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
'données' (ou les 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

Now "f", "g" and "h" are all attributes of class "C" that refer to
function objects, and consequently they are all methods of instances
of "C" --- "h" being exactly equivalent to "g".  Note that this
practice usually only serves to confuse the reader of a program.

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'il
est rare d'avoir une bonne raison d'utiliser des données globales dans
une méthode, il y a de nombreuses utilisations légitimes de la 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ée aussi son
*type*). Elle est stockée dans "objet.__class__".


9.5. Héritage
=============

Bien sûr, ce terme de "classe" ne serait pas utilisé s'il n'y avait
pas d'héritage. La syntaxe pour définir une sous-classe est de cette
forme :

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

The name "BaseClassName" must be defined in a namespace accessible
from the scope containing the derived class definition.  In place of a
base class name, other arbitrary expressions are also allowed.  This
can be useful, for example, when the base class is defined in another
module:

   class DerivedClassName(modname.BaseClassName):

L'exécution d'une définition de classe dérivée se déroule comme pour
une classe mère. Quand l'objet de la classe est construit, la classe
mère 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 se poursuit en regardant dans la classe mère.
Cette règle est appliquée récursivement si la classe mère est elle-
même dérivée d'une autre classe.

Il n'y a rien de particulier dans l'instanciation 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 mères 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
mères. 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 mère 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 "virtuelles").

Une méthode dans une classe dérivée peut aussi, en fait, vouloir
étendre plutôt que simplement remplacer la méthode du même nom de sa
classe mère. L'appel direct à la méthode de la classe mère s'écrit
simplement "BaseClassName.nomMethode(self, arguments)". C'est parfois
utile également aux clients (notez bien que ceci ne fonctionne que si
la classe mère est accessible en tant que "BaseClassName" dans la
portée globale).

Python définit deux fonctions primitives pour gérer l'héritage :

* utilisez "isinstance()" pour tester le type d'une instance :
  "isinstance(obj, int)" renvoie "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 classe :
  "issubclass(bool, int)" renvoie "True" car la classe "bool" est une
  sous-classe de "int". Cependant, "issubclass(float, int)" renvoie
  "False" car "float" n'est pas une sous-classe de "int".


9.5.1. Héritage multiple
------------------------

Python gère également une forme d'héritage multiple. Une définition de
classe ayant plusieurs classes mères est de cette forme :

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

For most purposes, in the simplest cases, you can think of the search
for attributes inherited from a parent class as depth-first, left-to-
right, not searching twice in the same class where there is an overlap
in the hierarchy. Thus, if an attribute is not found in
"DerivedClassName", it is searched for in "Base1", then (recursively)
in the base classes of "Base1", and if it was not found there, it was
searched for in "Base2", and so on.

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

Dynamic ordering is necessary because all cases of multiple
inheritance exhibit one or more diamond relationships (where at least
one of the parent classes can be accessed through multiple paths from
the bottommost class).  For example, all classes inherit from
"object", so any case of multiple inheritance provides more than one
path to reach "object".  To keep the base classes from being accessed
more than once, the dynamic algorithm linearizes the search order in a
way that preserves the left-to-right ordering specified in each class,
that calls each parent only once, and that is monotonic (meaning that
a class can be subclassed without affecting the precedence order of
its parents). Taken together, these properties make it possible to
design reliable and extensible classes with multiple inheritance.  For
more detail, see The Python 2.3 Method Resolution Order.


9.6. Variables privées
======================

Les membres "privés", qui ne peuvent être accédés que depuis
l'intérieur 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 considéré comme une
partie non publique de l'API (qu'il s'agisse d'une fonction, d'une
méthode ou d'un attribut 'données'). Il doit être vu comme un détail
d'implémentation pouvant faire l'objet de modifications futures sans
préavis.

Dès lors qu'il y a un cas d'utilisation valable pour avoir des
attributs privés aux classes (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 de la forme "__spam" (avec au moins deux tirets bas 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 ou
les premiers tirets-bas. Ce "découpage" 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.

Voir aussi:

  The private name mangling specifications for details and special
  cases.

Ce changement de nom est utile pour permettre à des sous-classes de
surcharger des méthodes sans casser les appels de méthodes à
l'intérieur d'une classe. 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)

L'exemple si dessus fonctionnerait même si "MappingSubclass"
introduisait un identifieur "__update" puisqu'il a été remplacé avec
"_Mapping__update" dans la classe "Mapping" et
"_MappingSubclass__update" dans la classe "MappingSubclass"
respectivement.

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.

Remarquez que le code que vous passez à "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 « données » nommés. L'approche idiomatique correspondante en
Python est d'utiliser des "dataclasses" :

   from dataclasses import dataclass

   @dataclass
   class Employee:
       name: str
       dept: str
       salary: int

   >>> john = Employee('john', 'computer lab', 1000)
   >>> john.dept
   'computer lab'
   >>> john.salary
   1000

A piece of Python code that expects a particular abstract data type
can often be passed a class that emulates the methods of that data
type instead.  For instance, if you have a function that formats some
data from a file object, you can define a class with methods "read()"
and "readline()" that get the data from a string buffer instead, and
pass it as an argument.

Instance method objects have attributes, too: "m.__self__" is the
instance object with the method "m()", and "m.__func__" is the
function object corresponding to the method.


9.8. 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 style 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 objet itérateur qui définit la méthode "__next__()",
laquelle accède aux é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
appeler la méthode "__next__()" en utilisant la fonction native
"next()". Cet exemple montre comment tout cela fonctionne :

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

Having seen the mechanics behind the iterator protocol, it is easy to
add iterator behavior to your classes.  Define an "__iter__()" method
which returns an object with a "__next__()" method.  If the class
defines "__next__()", then "__iter__()" can just return "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.9. 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 qu'il est appelé par "next()", 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

Anything that can be done with generators can also be done with class-
based iterators as described in the previous section.  What makes
generators so compact is that the "__iter__()" and "__next__()"
methods are created automatically.

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. La
combinaison de ces fonctionnalités rend très simple la création
d'itérateurs, sans plus d'effort que l'écriture d'une fonction
classique.


9.10. 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

   >>> 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 de bas de page ]-

[1] Il existe une exception : les modules disposent d'un attribut
    secret en lecture seule appelé "__dict__" qui renvoie le
    dictionnaire utilisé pour implémenter l'espace de nommage du
    module ; le nom "__dict__" est un attribut mais pas un nom global.
    Évidemment, si vous l'utilisez, vous brisez l'abstraction de
    l'implémentation des espaces de nommage. Il est donc réservé à des
    choses comme les débogueurs post-mortem.
