Plongeon dans les objets

Des objets, rien que des objets

Rappels de principes de base en programmation orientée objet

Instanciation

Un objet est créé à partir d’un moule, sa classe qui définit une structure (les attributs) et un comportement (les méthodes). Une classe définit un type d’objets. Un objet est instance d’une unique classe.

Encapsulation

Les données sont «cachées» au sein des objets, et leur accès est contrôlé par les méthodes. L’état d’un objet ne devrait pas être manipulé directement si un contrôle est nécessaire. L’objet peut être vu comme un fournisseur de services (plus que de données).

Encapsulation n’implique pas attributs privés comme dans certains langages de programmation. Un attribut privé ne protège pas d’une utilisation abusive, mai considère le développeur comme un danger. Python prend une approche différente : le développeur a un cerveau, et donc, faisons lui confiance.

Polymorphisme

Des objets respectant une même interface (la signature de méthodes suffit dans le cas de Python) peuvent être manipulés de manière générique, même si leur type exact est différent. Ce principe permet aussi la substitution d’une instance par une autre (tant que les interfaces sont compatibles).

Héritage

Mécanisme qui permet la réutilisation de définitions de base, comportements par défaut, et la spécialisation de certains comportements. La relation d’héritage entre deux classes ne se limite pas à une facilité de capitalisation de code. La classe qui hérite doit «être un» de la classe dont elle hérite: un chat «est un» animal, chat peut donc être une sous classe de animal. Mais une voiture ne doit pas hériter d’un moteur: une voiture n’est pas un moteur elle contient un moteur.

Objets et références

En Python, le monde est uniforme.

  • Tout est objet: les chaînes, les entiers, les listes, les fonctions, les classes, les modules, etc. Tout est réifié et manipulable dynamiquement.
  • Tout objet est manipulé par référence: une variable contient une référence vers un objet et un objet peut être référencé par plusieurs variables.
  • Une fonction, une classe, un module sont des espaces de nommage organisés de manière hiérarchique: un module contient des classes qui contiennent des fonctions (les méthodes).

Classes

Définition et instanciation

La définition d’une classe suit la règle des blocs (cf section ref{sub:chap1:blocs}) en utilisant le mot clé class. (Voir la section sur l’unification des types et des classes pour une explication sur l’héritage explicite de object).

>>> class Empty(object):
...     pass

Toute méthode est définie comme une fonction avec un premier argument (self) qui représente l’objet sur lequel elle sera appliquée à l’exécution. En Java ou C++, le this est définie implicitement, en Python il est explicite est c’est toujours le premier paramètre d’une méthode. Le nom est libre, mais on utilise en général self. Une méthode ne prenant pas de paramètres aura donc quand même un argument.

>>> class Dummy(object):
...     def hello(self):
...         print 'hello world!'

L’instanciation se fait sans mot clé particulier (il n’y a pas de new en Python). Il suffit de faire suivre un nom de classe de parenthèses (contenant ou non des paramètres) pour déclencher une instanciation. L’invocation se fait à l’aide de la notation pointée, un appel de méthode sur une variable référençant un objet.

>>> d = Dummy()
>>> d
<Dummy object at 0x...>
>>> d.hello()
hello world!

Définition d’attributs et du constructeur

Les attributs sont définis à leur première affectation. Une variable est considérée comme un attribut si elle est «rattachée» à l’objet: elle est accédée par self. Tout accès à un attribut ou à une méthode de l’objet depuis sa mise en oeuvre se fait obligatoirement par la variable self. Le constructeur est une «méthode» nommée __init__. Comme toute méthode, certain de ses paramètres peuvent avoir des valeurs par défaut. Cette possibilité est important car une classe a un seul constructeur en Python (contrairement à d’autre langages).

>>> class Compteur(object):
...     def __init__(self, v=0):
...         self.val = v
...     def value(self):
...         return self.val

En termes de génie logiciel (et de bon sens), il est recommandé de toujours initialiser l’état des objets (ses attributs) lors de son instanciation, donc dans le constructeur.

Visibilité des attributs et méthodes

Les attributs, comme les méthodes, ont par défaut une visibilité «publique» en Python. Dans le cas des instances de la classe Compteur, il est possible de manipuler directement la valeur de l’attribut (ou méthodes) val.

Une convention pour certains développeurs Python est de préfixer les attributs par un sougliné ‘_‘ pour annoncer qu’ils ne sont pas destinés à être utilisés par les clients. Il est à noter que c’est une convention et que rien n’empêche un utilisateur de lire un attribut dont le nom est préfixé par un souligné [1].

Enfin, l’utilisation de deux soulignés comme préfixe (et pas de souligné comme postfixe) d’un nom d’attribut (ou de méthode) permet de définir un attribut «privé» et donc non visible par les clients de la classe.

>>> class Foo(object):
...     def __init__(self):
...         self._a = 0
...         self.__b = 1
...
>>> f = Foo()
>>> f._a
0
>>> f.__b
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: Compteur instance has no attribute '__b'

Note

La philosophie de Python est de faire confiance au programmeur plutôt que de vouloir tout «blinder» comme dans d’autres langages (qui ne blindent pas grand chose au final).

Héritage

Python supporte l’héritage simple et l’héritage multiple. Dans une relation d’héritage, il est faut préciser le nom de la classe mère (entre parenthèses après le nom de la classe en cours de définition) et appeler explicitement le constructeur de la super classe.

>>> class A(object):
...     def __init__(self, n='none'):
...          self._name = n
...     def name(self):
...         return self._name

>>> class B(A):
...      def __init__(self, val=0, n='none'):
...          A.__init__(self, n)
...          self._wheels = val
...      def wheels(self):
...          return self._wheels

Dans le cas de l’héritage multiple, il suffit de préciser les différentes classes mères et d’appeler leurs constructeurs respectifs.

>>> class C(object):
...     def __init__(self, t=''):
...         self._title = t
...     def title(self):
...         return self._title

>>> class D(A, C):
...     def __init__(self, n='none', t=''):
...         A.__init__(self, n)
...         C.__init__(self, t)
...     def fullname(self):
...         return self._title + ' ' + self._name

L’exemple suivant donne une utilisation des trois classes précédentes. Il n’y a rien de particulier à l’utilisation que la classe ait été définie entièrement ou par héritage ne change rien.

>>> a = A('raphael')
>>> print a.name()
raphael
>>> b = B(4, 'car')
>>> print b.name()
car
>>> print b.wheels()
4
>>> d = D(t='dr')
>>> print d.fullname()
dr none

Héritage multiple problématique

L’héritage multiple pose problème lorsque deux classes héritées offre une méthode portant le même nom. Dans ce cas, il est possible de préfixer les méthodes par le nom de la classe qui les définies ou d’utiliser des alias de méthodes pour résoudre les conflits.

>>> class X(object):
...     def name(self):
...         return 'I am an X'

>>> class Y(object):
...     def name(self):
...         return 'I am an Y'

>>> class Z(X, Y):
...     xname = X.name
...     yname = Y.name
...     def name(self):
...         return 'I am an Z, ie ' + self.xname() + \
...             ' and ' + self.yname()

L’exemple suivant propose une utilisation des trois classes X, Y, Z précédentes avec une invocation de la méthode name() sur les trois instances créées.

>>> for clss in [X, Y, Z]:
...     obj = clss()
...     print obj.name()
...
I am an X
I am an Y
I am an Z, ie I am an X and I am an Y

Classes vs modules

Il semblerait que la vision suivante doit bien acceptée de la communauté de développeurs Python:

  • Utiliser des classes lorsque
    • Les données représentent un état et doivent être protégées.
    • Les traitements et données sont fortement liés (les données évoluent).
  • Utiliser des modules de fonctions lorsque
    • Les traitements et données sont «indépendants» (ex: data mining).
    • Les données sont stockées dans des BD ou fichiers, et non modifiées.

Structures des objets

Introspection simple

La fonction dir() donne le contenu de tout objet (liste de ses attributs et méthodes). Associée à l’attribut __doc__, cela fournit une documentation de base sur tout objet. D’où l’intérêt de bien auto-documenter le code (voir section ref{sub:chap1:doc}).

>>> dir(Compteur)
['__class__', ... '__init__', ... 'value']
>>> c = Compteur()
>>> dir(c)
['__class__', ... '__init__', ... 'val', 'value']

La fonction type() donne quant à elle le type d’une référence.

>>> type(c)
<class 'Compteur'>
>>> type([1, 2])
<type 'list'>

Classes et attributs

Une classe ne contient que des attributs. Elle peut être vue comme un simple dictionnaire contenant des associations nom / référence. Une fonction de la classe (ou méthode) est en fait un attribut qui est appelable (callable).

>>> class OneTwoThree(object):
...     value = 123             # reference un entier
...     def function(self):     # reference une fonction
...         return self.value
...
>>> ott = OneTwoThree()
>>> dir(ott)
['__class__', ... '__init__', ... 'function', 'value']
>>> ott.function
<bound method OneTwoThree.function of <OneTwoThree object at 0x...>>

Dans le cas d’un conflit de nom entre un attribut et une méthode, la priorité va à l’attribut. Ici, l’interpréteur explique que l’on ne peut utiliser l’attribut name comme si c’était une fonction (ou méthode) car c’est une chaîne, donc on ne peut demander son exécution (object is not callable). La méthode name est masquée par l’attribut portant le même nom car celui-ci sera initialisé après la définition de la méthode.

>>> class Conflict(object):
...     def __init__(self):
...         self.name = 'Conflict'
...     def name(self):
...         return 'You will never get this string!'
...
>>> c = Conflict()
>>> c.name()
Traceback (most recent call last):
  ...
TypeError: 'str' object is not callable

Dans ce cas, L’utilisation d’attributs «protégés» est donc une bonne idée qui a pour conséquence d’éviter les conflits de noms entre attributs et méthodes (même si dans le cas présent, je reconnais que l’existence d’une méthode name est discutable).

>>> class NoConflict(object):
...     def __init__(self):
...         self._name = 'NoConflict'
...     def name(self):
...         return self._name
...
>>> c = NoConflict()
>>> print c.name()
NoConflict

Il est possible de définir dynamiquement des attributs sur une classe ou une instance. Ce genre de facilité ne se limite pas à l’aspect ludique, c’est parfois la meilleure solution à un problème. Toutefois, attention où l’on met les pieds lorsque l’on modifie dynamiquement des objets.

>>> class Empty(object):
...     pass
>>> Empty.value = 0
>>> def funct(self):
...     self.value += 1
...     return self.value
...
>>> Empty.funct = funct
>>> dir(Empty)
['__class__', ... '__init__', ... 'funct', 'value']
>>> e = Empty()
>>> e.funct()
1

Quelques attributs notoires

  • __doc__ contient la documentation de la classe, de l’objet, de la fonction, du module, etc.
  • __module__ contient le nom du module contenant la définition de la classe, de l’objet, de la fonction, du module, etc.
  • __name__ contient le nom de la fonction ou de la méthode.
  • __file__ contient le nom du fichier contenant le code du module.

Les objets, version avancée

Un peu de réflexion

Les techniques de réflexion permettent de découvrir (introspection) et de manipuler dynamiquement et automatiquement les attributs (et fonctions) d’un objet. Ces techniques sont utilisables, par exemple, pour le chargement de code dynamique comme la mise en oeuvre d’un mécanisme de plug-ins. Elles sont aussi couramment utilisés dans les environnements de développement qui font de la complétion automatique (il faut bien aller chercher l’information quelque part). Il existe trois fonctions de base en Python:

  • hasattr() test si un attribut existe,
  • getattr() donne accès à la valeur d’un attribut,
  • setattr() fixe la valeur d’un attribut (avec création si l’attribut est inexistant).

Dans l’exemple suivant, nous manipulons l’instance étendue de la classe Empty: après avoir testé son existence, nous récupérons dynamiquement la méthode nommée funct pour en demander ensuite l’exécution.

>>> hasattr(e, 'funct')
True
>>> f = getattr(e, 'funct')
>>> f
<bound method Empty.funct of <Empty object at 0x...>>
>>> f()
2

Il est possible de faire la même chose en demandant à la classe la fonction funct. Mais, dans ce cas, pour demander son exécution il faut passer l’objet e sur lequel doit s’appliquer l’appel. Cette version illustre bien le pourquoi du premier argument (self) des méthode.

>>> hasattr(Empty, 'funct')
True
>>> f = getattr(Empty, 'funct')
>>> f
<unbound method Empty.funct>
>>> f(e)
3

Enfin, nous définissons un nouvel attribut name dont la valeur est fixée à myempty.

>>> setattr(e, 'name', 'myempty')
>>> e.name
'myempty'

Un peu plus de réflexion avec inspect

Le module inspect offre des moyens supplémentaires pour l’introspection, par exemple découvrir ce que contient une classe, un objet ou un module.

>>> import inspect

L’exemple suivant récupère les membres de la classe E puis les membres d’une instance de cette classe. Pour chaque membre de l’objet (classe ou instance), un tuple est fourni contenant le nom de l’attribut et sa valeur. Nous pouvons constater que la différence se limite à l’état de la fonction f: non liée dans le cas de la classe, donc que l’on ne peut exécuter directement, et liée à l’instance dans le cas de l’objet que l’on peut donc exécuter directement après un getattr.

>>> class E(object):
...     def f(self):
...             return 'hello'
...
>>> e = E()

>>> inspect.getmembers(E)
[('__class__', <type 'type'>), ... ('f', <unbound method E.f>)]

>>> inspect.getmembers (e)
[('__class__', <class 'E'>), ... ('f', <bound method E.f of <E object at 0x...>>)]

Ce module permet aussi de savoir ce que l’on est en train de manipuler: quel est le type d’un objet (classe, instance, attribut). La fonction ismethod() permet de savoir si un objet donné est, ou non, une méthode (liée ou non).

>>> inspect.isclass(E)
True
>>> f = getattr(e, 'f')
>>> inspect.isfunction(f)
False
>>> inspect.ismethod(f)
True
>>> F = getattr(E, 'f')
>>> inspect.ismethod(F)
True

L’association de l’introspection de base et du module inspect permet d’automatiser l’utilisation des méthodes d’une instance: invocation générique d’une méthode récupérée dynamiquement après contrôle que c’est bien une méthode. L’exemple suivant montre aussi les deux manières d’invoquer une méthode, et la signification du self: a.foo() est en fait traduit en A.foo(a) par l’interpréteur (considérant que a est une instance de la classe A).

>>> f1 = getattr(e, 'f')
>>> f2 = getattr(E, 'f')
>>> if inspect.ismethod(f1):
...     f1()  # `f1' est liee a `e'
...
'hello'
>>> if inspect.ismethod(f2):
...     f2(e)  # `f2' n'est pas liee, argument 1 == self
...
'hello'

Le module inspect permet même d’accéder dynamiquement au code source d’un objet. Toutefois, cela n’est pas valable pour le code saisi en interactif (ce qui est somme toute normale vu qu’il n’est pas stocké dans un fichier).

>>> from examples import Conflict

Quelques fonctions de inspect

La fonction getfile() donne le nom du fichier de définition de l’objet (TypeError si cette opération est impossible).

>>> inspect.getfile(Conflict)
'examples.py'

La fonction getmodule() donne le nom du module définissant l’objet (sans garantie par exemple dans le cas du code dynamiquement créé).

>>> inspect.getmodule(Conflict)
<module 'examples' from 'examples.py'>

La fonction getdoc() retourne la documentation de l’objet.

>>> inspect.getdoc(Conflict)
'Illustration de conflit attribut / methode'

La fonction getcomments() retourne la ligne de commentaire précédant la définition de l’objet.

La fonction getsourcelines() donne un tuple contenant la liste des lignes de code source définissant l’objet passé en paramètre et la ligne du début de la définition dans le fichier.

>>> lines, num = inspect.getsourcelines(Conflict)
>>> for i, l in enumerate(lines):
...     print num + i, l
...
34 class Conflict(object):
35     '''Illustration de conflit attribut / methode'''
36     def __init__(self):
37         self.name = 'Conflict'
38     def name(self):
39         return 'You will never get this string!'

La fonction getsource() est similaire à la précédente, mais retourne uniquement le code de la définition sous la forme d’une chaîne de caractères.

Les exceptions en python

Définition et levée

Les exceptions sont des objets comme les autres, donc définies par des classes comme les autres (ou presque). Elles sont simplement un penchant pour ne rien faire (les classes d’exception contiennent rarement des traitements). La définition minimale d’une classe pour une exception revient à étendre la classe de base Exception.

>>> class MonErreur(Exception):
...     pass
...

La mot-clé raise permet de lever une exception (définie par l’usager ou standard). Si l’on souhaite associé de l’information à une exception, il suffit de faire suivre le type de l’exception d’un message (séparé par une virgule). [2]

>>> raise MonErreur
Traceback (most recent call last):
  ...
MonErreur
>>> raise MonErreur()
Traceback (most recent call last):
  ...
MonErreur
>>> raise NameError, 'cela coince'
Traceback (most recent call last):
  ...
NameError: cela coince

Une exception peut aussi contenir des attributs et méthodes (en général pour fournir de l’information). Ici, la méthode standard __str__ (traduction de l’objet en chaîne) et utilisée pour accéder facilement à la valeur contenue dans l’exception (par défaut le mécanisme d’exception appelle cette méthode). Pour fournir de l’information à une exception, il suffit de l’instancier en passant des paramètres au constructeur lors de l’utilisation de raise.

>>> class MonErreurToo(Exception):
...     def __init__(self, val=None):
...         self._value = val
...     def __str__(self):
...         return str(self._value)
...
>>> raise MonErreurToo(12)
Traceback (most recent call last):
  ...
MonErreurToo: 12

Traitement des exceptions

Il y a deux possibilités complémentaires pour le traitement des exceptions en Python:

  • quoi faire en cas de problème,
  • que faut il toujours faire même s’il y a un problème.

Gestion des problèmes, le try ... except permet en cas de levée d’exception, de pouvoir tenter de traiter le problème. Il peut y avoir plusieurs clauses except pour traiter différents cas exceptionnels. La clause else est optionnelle est permet de faire un traitement si tout s’est bien passé (le else et le except sont en exclusion mutuelle). Enfin, la clause optionnelle finally permet d’effectuer un traitement qu’il y ait eu ou non levée d’exceptionfootnote{L’utilisation conjointe des clauses except et finally est possible depuis la version 2.5. Dans les version précédentes il faut imbriquer les des constructions.}.

try:
    <instructions>
(except <exception>:
    <instructions traitement exception>)+
[else:
    <instructions si traitement correct>]
[finally:
    <instructions dans tous les cas>]

L’utilisation de la clause else est intéressante car elle permet d’isoler la ligne du bloc try qui peut lever une exception des lignes qui en dépendent.

L’identification d’une <exception> peut prendre plusieurs formes:

  • ExceptionType est utile lorsque seul le type de l’exception est important, par exemple parce qu’elle ne contient pas d’information~;
  • ExceptionType as variable permet de récupérer l’exception du type spécifié dans variable et donc d’exploiter les données et traitements de l’exception.

Lecture d’un fichier texte

Il est très pythonesque d’utiliser la construction ouverture d’une ressource, utilisation dans un bloc try et libération de la ressource dans un bloc finally. On ouvre le fichier puis quoi qu’il arrive on ferme le fichier par la suite. Un fichier devrait toujours être fermé après utilisation.

>>> fichier = open('/tmp/foo.txt')
>>> try:
...     for line in fichier.readlines():
...         print line,
... finally:
...     fichier.close()
...
hello world!
bonjour le monde!
au revoir le monde!

Gestion de plusieurs exceptions

L’affichage va dépendre de l’exception qui est levée (elle même levée de manière aléatoire).

>>> import random
>>> class Impair(Exception): pass
>>> class Pair(Exception): pass

>>> try:
...     if int(random.random() * 100) % 2:
...         raise Impair()
...     else:
...         raise Pair()
... except Pair:
...     print "c'est un nombre pair"
... except Impair:
...     print "c'est un nombre pair"
...
c'est un nombre pair

Attention

Un return dans le finally masque un return dans le try.

Traitement d’exceptions et héritage

Petit problème courant, attention à l’ordonnancement des except! Il faut toujours, dans le cas où les exceptions sont définies par une relation d’héritage, commencer par traiter les exceptions les plus spécifiques. Dans le cas contraire, la gestion des exceptions les plus générales masque les traitements relatifs aux exceptions les plus spécifiques qui ne sont jamais utilisés.

>>> class A(Exception): pass
>>> class B(A): pass

>>> for cls in [A, B]:
...     try: raise cls()
...     except B: print 'B',
...     except A: print 'A',
A B
>>> for cls in [A, B]:
...     try: raise cls()
...     except A: print 'A',
...     except B: print 'B',
A A

Toujours à propos des objets

Cette section est pour certain aspects plutôt à lire une fois que l’on est familier avec Python et avec la notion d’objet.

Unification des types et des classes

Depuis Python 2.2, les types et les classes ont été unifiés en Python. C’est-à-dire qu’il n’y a plus de différence fondamentale entre les types de base et les classes définies par l’utilisateur. Le bénéfice de cette unification est qu’il est désormais possible de définir des classes héritant des types de base.

Suite à cette unification, et pour ne pas briser la compatibilité avec les programmes existants, la déclaration des classe a un peu évolué (c’est la version que nous avons utilisé jusqu’ici). Pour préciser qu’une classe est définie avec la nouvelle forme, il faut la faire hériter de object, d’un type de base ou d’une classe héritant de object). Attention, toutes les classes de la librairie standard ne suivent pas toute la nouvelle forme et sont donc considérées comme «différentes».

L’exemple suivant présente les deux formes de déclaration de classes simultanément. L’utilisation de la fonction type sur des instances de ces deux classes illustre bien la différence de statut vis-à-vis de l’interpréteur.

>>> class Foo:
...     pass
...
>>> class Bar(object):
...     pass
...
>>> f = Foo()
>>> b = Bar()
>>> type(f)
<type 'instance'>
>>> type(b)
<class 'Bar'>

Avec la nouvelle forme, l’utilisation de méthodes fournies par les super-classes est aussi modifiée. Lors de la redéfinition d’une méthode dans une sous-classe, la fonction super permet d’accéder à la définition (originale) de cette méthode dans la super-classe (ou une des super-classes).

La fonction super prend comme premier paramètre le type à partir duquel doit se faire la recherche [3] et comme second paramètre self. Contrairement à l’ancienne forme, il n’est plus nécessaire de passer self en premier argument de la méthode (voir la section ref{sub:chap3:heritage}). La fonction super est aussi utilisable dans la définition des constructeurs.

>>> class Bar(object):
...     def bar(self):
...         print 'Bar.bar'
...
>>> class BarToo(Bar):
...     def bar(self):
...         print 'BarToo.bar'
...         super(BarToo, self).bar()
...
>>> f2 = BarToo()
>>> f2.bar()
BarToo.bar
Bar.bar

Définition de propriétés

Avec l’unification classe / type, la notion de propriété est apparue en Python. Une propriété permet de manipuler un attribut à l’aide d’accesseurs de manière transparente (c’est-à-dire que l’on ne dit pas explicitement que l’on utilise les accesseurs). Une propriété est définie à l’aide de la fonction property dans le contexte d’une classe qui étend (même indirectement) object.

La fonction property prend en paramètre la fonction pour la lecture, la fonction pour l’écriture, la fonction pour la suppression de la valeur associée et une chaîne de documentation. Tous les paramètres sont optionnels (eh oui, il est même possible de définir une propriété qui ne fait rien pas même être lue).

Dans l’exemple suivant, la classe A est définie avec une propriété value en lecture et écriture: elle est définie avec une méthode pour la lecture et une pour l’écriture. La classe B est quant à elle définie avec une propriété value en lecture seule: seule une méthode de lecture est fournie.

>>> class A(object):
...     def __init__(self, val=0):
...         self._value = val
...     def get_value(self):
...         return self._value
...     def set_value(self, val):
...         self._value = val
...     value = property(get_value, set_value)
...
>>> class B(object):
...     def __init__(self, val):
...         self._value = val
...     def get_value(self):
...         return self._value
...     value = property(get_value)
...

Une propriété est utilisée de la même manière qu’un attribut. Dans le cas courant l’utilisation est peu différente d’un attribut classique, même les méthode de lecture / écriture pourrait très bien aller chercher l’information dans un fichier ou autre. Enfin, la tentative d’écriture la propriété value sur une instance de B se solde par une exception. Il est donc intéressant d’utiliser des propriétés pour faire du contrôle d’accès sur des attributs.

>>> a = A(1)
>>> a.value
1
>>> a.value = 2
>>> a.value
2
>>> b = B(2)
>>> b.value
2
>>> b.value = 1
Traceback (most recent call last):
  ...
AttributeError: can't set attribute

Note

Le choix entre un attribut “publique” et une propriété se résume à: “est ce une valeur calculée?” Si une valeur est stockée, utiliser un attribut, si elle doit être calculée, utiliser une propriété. On retrouve ici une illustration de la philosophie de Python: faire confiance.

Pour ne pas trop «polluer» la classes, il est courant de définir en Python les fonctions utilisées par les propriétés comme des fonctions internes. Une fois l’attribut value définie comme une propriété, la fonction value n’est plus visible dans la définition de la classe.

>>> class Dummy(object):
...     def __init__(self, value, tax=1.196):
...         self._value = value
...         self.tax = tax
...     def value():
...         doc = "The value with taxes."
...         def fget(self):
...             return self._value * self.tax
...         def fset(self, value):
...             self._value = value
...         return locals()
...     value = property(**value())
...
>>> d = Dummy(20)
>>> d.value
23.919999999999998

En utilisant cette approche la documentation n’est plus polluée.

>>> help(Dummy)
Help on class Dummy in module __main__:

class Dummy(__builtin__.object)
|  Methods defined here:
|
|  __init__(self, value, tax=19.600000000000001)
|
|  ----------------------------------------------------------------------
|  Data descriptors defined here:
|
|  value
|      The value property.

Décorateurs

Le concept de décorateur de fonction (ou de méthode) a été introduit dans la version 2.4 (PEP 318). La notion de décorateur n’est pas des plus simple à expliquer, mais on peut la voire comme une fonction qui applique des traitements sur une autre fonction pour en changer le comportement. Un décorateur peut donc être toute fonction prenant en argument une fonction (ou tout objet «appelable»). L’utilisation d’une fonction comme décorateur d’une autre fonction se fait en utilisant le nouvel opérateur @. En considérant un décorateur decorateur et une fonction foo les deux déclarations suivantes sont équivalentes.

@decorateur
def foo():
    pass

def foo():
    pass
foo = decorateur(foo)

Python propose quelques décorateurs par défaut par exemple pour les propriété et pour les méthodes de classe. Le décorateur de propriété ne peut être utilisé que pour une propriété en lecture seule.

>>> class B(object):
...     def __init__(self, val):
...         self._value = val
...     @property
...     def value(self):
...         return self._value

>>> b = B(2)
>>> b.value
2
>>> b.value = 1
Traceback (most recent call last):
  ...
AttributeError: can't set attribute

L’exemple suivant prend le grand classique de la trace des appel de fonction pour illustrer la définition et l’utilisation d’un décorateur. La fonction trace définie une fonction d’emballage (un wrapper) _trace qui va, en plus du comportement normal de la fonction qu’elle emballe, afficher le nom et les arguments de celle-ci. L’utilisation de trace permet donc de substituer la définition de base d’une fonction par une version incluant la trace: trace retourne tout simplement une fonction.

Notre fonction trace va être utilisable sur toute fonction comme un décorateur pour afficher sur la sortie standard les appels à cette fonction. Ici, nous traçons la fonction bar, dont la définition est tout ce qu’il y a de plus normal, si ce n’est la déclaration @trace.

>>> def trace(fct):
...     def _trace(*args):
...         print '<entering %s%s >' % (fct.__name__, args)
...         return fct(args)
...     return _trace
...
>>> @trace
... def bar(name):
...     print 'hello %s' % name
...
>>> bar('raphael')
<entering bar('raphael',) >
hello raphael

Il est à noter qu’une fonction (ou méthode) peut être décorée plusieurs fois.

Exercices

Premières classes

Implanter des classes Pile et File utilisant les liste comme structure de données interne et respectant l’interface suivante (utiliser l’héritage).

class Base(object):
    def pop(self):
        pass
    def push(self, elt):
        pass

Reprendre l’exercice ref{sub:chap2:exo2} en implantant les fonctionnalités au sein d’une classe.

Design pattern état

L’état d’un objet a de type A est implanté par plusieurs classes. L’instance de ces classes qui représente l’état de l’objet varie au cours du cycle de vie de l’objet. Les méthodes de l’objet sont déléguées à l’instance représentant l’état à un moment donné.

Implanter ce design pattern pour représenter un feu tricolore (une classe d’état par couleur, une méthode pour changer la couleur du feu et une méthode qui donne le droit de passage).

[1]Il est vrai que dans le cas de Python la notion de «protégé» repose sur la confiance, mais si vous roulez à 180 km/h en ville qui est responsable, la voiture ou vous ?
[2]Les exceptions sont levées comme des objets. Une classe est définie par un objet dans l’interpréteur. Donc la définition de la classe peut être levée comme une exception. Ce qui n’est pas si choquant pour les cas simple (exception avec message d’erreur).
[3]Dans le cas d’une hiérarchie à plusieurs niveaux, la recherche de la méthode hérité ne se fait pas nécessairement à partir de la super-classe, mais elle peut se faire à partir de la super-super-classe par exemple.