Cela fait un moment que l'on n'a pas fait de TP sur ce forum ! Pour rattraper le temps perdu, je vous propose aujourd'hui une activité de niveau avancé qui va vous faire aborder plusieurs notions de notre langage favori sur lesquelles on travaille peu d'ordinaire, et vous faire regarder Python non pas en tant que simple utilisateur, mais plutôt, pour une fois, de l'intérieur.
Avant de commencer, voici quelques notions sur lesquelles nous allons nous pencher dans ce TP :
Les properties, évidemment, puisque ce TP tourne autour de leur mécanisme,
Les descripteurs, qui sont une notion d'assez bas niveau (et, comme vous allez le voir, puissante en Python) que vous ne connaissez peut-être pas,
Les décorateurs dont nous allons voir une utilisation astucieuse.
Introduction
Rappels sur les properties
Il n'est pas rare les développeurs POO de tout poil habitués à C++ ou Java reprochent à Python son aspect tout-est-public, arguant qu'à cause de cela, on ne peut exercer aucun contrôle sur les attributs de nos classes, parce que n'importe qui peut modifier n'importe quel attribut de n'importe quelle classe, « à moins de faire des trucs un peu dégueu à bas niveau ».
En fait, Python dispose d'un mécanisme qui permet, entre autres, de contrôler l'accès en lecture et en écriture aux attributs d'une classe donnée, et ce de façon totalement transparente : les properties. Je vous ai mis la version longue du « pourquoi du comment » dans la balise secret suivante. Si vous connaissez déjà ce mécanisme vous pouvez passer à la suite.
Comme vous le savez déjà, si j'instancie cette classe, j'obtiendrai un objet avec les attributs duquel je pourrai jouer comme bon me semble. En fait, rien ne m'empêcherait de faire des choses qui n'ont aucun sens, comme assigner une chaîne de caractères à rect.width et un nombre flottant strictement négatif à rect.height. Python ne bronchera pas, sauf qu'il déclenchera une erreur bizarre lorsque je voudrai calculer l'aire de mon rectangle, par exemple.
Un premier moyen de garder un peu de contrôle sur mes attributs serait de faire quelque-chose comme ceci :
class Rectangle:
def __init__(self, width, height):
if not isinstance(width, int) or not isinstance(height, int):
raise TypeError ("width et height doivent être entiers")
if width < 0 or height < 0:
raise ValueError("width et height doivent être positifs")
self.__width = width
self.__height = height
def get_width(self):
return self.__width
def get_height(self):
return self.__height
def set_width(self, val):
if not isinstance(val, int) or val < 0:
raise TypeError("width doit être un entier positif")
self.__width = val
def set_height(self, val):
if not isinstance(val, int) or val < 0:
raise TypeError("height doit être un entier positif")
self.__height = val
L'interface de cette classe est beaucoup plus lourde à utiliser que la précédente (4 méthodes à retenir pour l'utilisateur...), mais au moins, elle a le mérite de ne pas nous obliger à faire une confiance aveugle à l'utilisateur : s'il fait n'importe quoi, son programme plantera.
Les properties sont un moyen de simplifier l'interface de votre classe tout en gardant le contrôle sur vos attributs :
class Rectangle:
def __init__(self, width, height):
if not isinstance(width, int) or not isinstance(height, int):
raise TypeError ("width et height doivent être entiers")
if width < 0 or height < 0:
raise ValueError("width et height doivent être positifs")
self.__width = width
self.__height = height
@property
def width(self):
return self.__width
@property
def height(self):
return self.__height
@width.setter
def width(self, val):
if not isinstance(val, int) or val < 0:
raise TypeError("width doit être un entier positif")
self.__width = val
@height.setter
def height(self, val):
if not isinstance(val, int) or val < 0:
raise TypeError("height doit être un entier positif")
self.__height = val
Testons cette classe :
Python 3.2.2 (default, Nov 21 2011, 16:51:01)
[GCC 4.6.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import rect
>>> r = rect.Rectangle(13, 37)
>>> r.width
13
>>> r.height
37
>>> r.width = 3
>>> r.height = -14
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "rect.py", line 29, in height
raise TypeError("height doit être un entier positif")
TypeError: height doit être un entier positif
>>> r.height = 14
>>> print(r.width, ".", r.height)
3 . 14
Et mieux encore. Imaginons maintenant que je veuille donner à mon rectangle un attribut pour représenter son aire. Il est inconcevable de laisser à l'utilisateur la possibilité d'écrire dans cet attribut, étant donné que dans ce cas, on ne pourrait plus garantir que l'aire du rectangle est égale au produit de sa hauteur et de sa largeur.
Les properties nous fournissent une solution élégante à ce problème :
>>> r = Rectangle(13, 37)
>>> r.area
481
>>> r.area = 28
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
Les properties font partie des built-ins de Python. C'est-à-dire qu'elles ont été implémentées en C et qu'elles sont accessibles sans importer le moindre module. Elles sont donc une feature de Python d'assez bas niveau. Ce TP va consister à réimplémenter de A à Z ce mécanisme en pur Python, mais rassurez-vous, nous allons faire tout cela pas à pas, et on en profitera pour apprendre deux-trois nouveaux trucs au passage.
L'interface de base
Dans cette partie, je vais essayer de vous aider à démarrer. Un autre moyen que les décorateurs pour créer une property est d'invoquer directement un constructeur comme ceci :
class Rectangle:
def __init__(self, width, height):
if not isinstance(width, int) or not isinstance(height, int):
raise TypeError ("width et height doivent être entiers")
if width < 0 or height < 0:
raise ValueError("width et height doivent être positifs")
self.__width = width
self.__height = height
def get_width(self):
return self.__width
def set_width(self, val):
if not isinstance(val, int) or val < 0:
raise TypeError("width doit être un entier positif")
self.__width = val
width = property(fget=get_width, fset=set_width)
Ce code nous dévoile clairement la nature des properties. Il s'agit d'un objet, qui encapsule ici deux fonctions fset et fget, et qui est contenu dans un attribut statique de la classe Rectangle, c'est-à-dire un attribut qui est partagé par toutes ses instances.
Cela signifie que notre code commencera par ceci :
class prop():
""" Classe décrivant une property. """
def __init__(self, fget=None, fset=None):
self.__fget = fget
self.__fset = fset
Les descripteurs
Un descripteur est un objet capable de contrôler ce qui se passe lorsque l'on accède à sa référence en lecture ou en écriture. Par exemple, si l'objet obj a le descripteur desc dans ses attributs statiques, alors desc est capable entre autres de contrôler ce qui se passe lorsque l'on l'assigne à une variable, (var=obj.desc), ou lorsque l'on veut remplacer sa référence par une autre (obj.desc=var).
Concrêtement, un descripteur n'est rien d'autre qu'un objet qui surcharge ses méthodes spéciales __get__ ou __set__ et __delete__.
Je vous montre comment cela se passe pour l'accès en lecture, la documentation devrait suffire pour le reste :
>>> class Foo:
... def __get__(self, instance, owner):
... print("Accès à l'attribut Foo de l'instance", instance, "de la classe", owner.__name__)
... return self
...
>>> class MaClasse:
... foo = Foo()
...
>>> obj = MaClasse()
>>> obj.foo
Accès à l'attribut Foo de l'instance <__main__.MaClasse object at 0xb70b11cc> de la classe MaClasse
<__main__.Foo object at 0xb70b12cc>
Sympathique, non ?
Allez, au travail !
1. Surchargez la méthode __get__ de la classe prop, de façon qu'elle appelle l'accesseur qu'elle encapsule chaque fois que l'on accède à elle en lecture. Si une propriété n'encapsule pas de méthode fget, alors elle lève une exception de type AttributeError indiquant que l'attribut n'est pas accessible en lecture.
2. Surchargez la méthode __set__ de la classe prop, de façon qu'elle appelle le mutateur qu'elle encapsule chaque fois que l'on accède à elle en écriture. Si une propriéte n'encapsule pas de méthode fset, alors elle lève une exception de type AttributeError indiquant que l'attribut n'est pas accessible en écriture.
Bravo ! Vous venez de coder les fameux « 80% de fonctionnalités qui demandent 20% du code ». En particulier, cela signifie que vos properties faites maison sont déjà parfaitement utilisables sur n'importe quelle classe, pourvu que vous la créiez avec la syntaxe width=property(get_width,set_width).
Il ne nous reste plus qu'à fignoler notre classe pour qu'elle se comporte exacement comme la classe property.
Les autres attributs des propriétés
3. Donnez à la classe prop le contrôle de ce qui se produit lorsque l'on supprime une propriété au moyen de la syntaxe delfoo.bar_prop. Notez que si la propriété n'encapsule pas de fonction fdel, aucune exception n'est levée.
4. Lorsque l'on appelle la fonction help() sur une property, celle-ci affiche la documentation de la fonction fget(), ou bien l'attribut __doc__ de la propriété, si celui-ci a été passé au constructeur de la classe property. Implémentez ce comportement.
L'interface avancée : les décorateurs
5. Les propriétés disposent de deux méthodes setter() et deletter() qui s'utilisent comme des décorateurs (voir plus haut). Implémentez ces méthodes.
6. Question-piège : implémentez la création d'une propriété au moyen du décorateur @prop, comme dans l'exemple du début. Attention, c'est assez compliqué…
Et voilà ! Vous venez de réimplémenter les properties de Python dans leur totalité.
class prop(object):
""" Classe décrivant une property. """
def __init__(self, fget=None, fset=None):
self.__fget = fget
self.__fset = fset
def __get__(self, instance, classe):
if self.__fget is None:
raise AttributeError('Proteger en lecture')
return getattr(instance, self.__fget.__name__)()
def __set__(self, instance, valeur):
if self.__fset is None:
raise AttributeError('Proteger en ecriture')
getattr(instance, self.__fset.__name__)(valeur)
class Rectangle(object):
def __init__(self, width, height):
if not isinstance(width, int) or not isinstance(height, int):
raise TypeError ("width et height doivent être entiers")
if width < 0 or height < 0:
raise ValueError("width et height doivent être positifs")
self.__width = width
self.__height = height
def get_width(self):
return self.__width
def set_width(self, val):
if not isinstance(val, int) or val < 0:
raise TypeError("width doit être un entier positif")
self.__width = val
width = prop(fget=get_width, fset=set_width)
r = Rectangle(15, 5)
print r.width
r.width = 20
print r.width
# -*- coding:utf8 -*-
class prop(object):
""" Classe décrivant une property. """
def __init__(self, fget=None, fset=None, fdel=None):
self.__fget = fget
self.__fset = fset
self.__fdel = fdel
def __get__(self, instance, classe):
if self.__fget is None:
raise AttributeError('Proteger en lecture')
return getattr(instance, self.__fget.__name__)()
def __set__(self, instance, valeur):
if self.__fset is None:
raise AttributeError('Proteger en ecriture')
getattr(instance, self.__fset.__name__)(valeur)
def __delete__(self, instance):
if self.__fdel is None:
raise AttributeError('suppression impossible')
getattr(instance, self.__fdel.__name__)()
class Rectangle(object):
def __init__(self, width, height):
if not isinstance(width, int) or not isinstance(height, int):
raise TypeError ("width et height doivent être entiers")
if width < 0 or height < 0:
raise ValueError("width et height doivent être positifs")
self.__width = width
self.__height = height
def get_width(self):
return self.__width
def set_width(self, val):
if not isinstance(val, int) or val < 0:
raise TypeError("width doit être un entier positif")
self.__width = val
def del_width(self):
print "suppression de l'attribut __width"
del self.__width
width = prop(fget=get_width, fset=set_width, fdel=del_width)
r = Rectangle(15, 5)
print r.width
r.width = 20
print r.width
del r.width
print r.width
Du coup, j'ai pas du tout compris pourquoi fred1599 s'est cassé la tête à ce point.
Parce-que getattr demande une chaîne de caractères.
En effet ce que tu as fais doit fonctionner.
par contre je préfère isNone à == None
Edit :Quoique après réflexion, je ne comprend pas pourquoi tes fonction __fget, etc... prennent une instance comme argument, sachant que l'instance est l'objet créé grâce à la classe Rectangle.
La notation serait instance.methode ou instance.attribut
Parce-que getattr demande une chaîne de caractères.
Pourquoi as-tu besoin de getattr ?
Citation : fred1599
Edit :Quoique après réflexion, je ne comprend pas pourquoi tes fonction __fget, etc... prennent une instance comme argument, sachant que l'instance est l'objet créé grâce à la classe Rectangle.
La notation serait instance.methode ou instance.attribut
De rien.
Si la formule "recoder une feature de Python pour jouer avec des notions avancées" vous plaît, j'ai déjà une ou deux autres idées de TP (beaucoup plus longs, par contre) pour la prochaine fois.
Si la formule "recoder une feature de Python pour jouer avec des notions avancées" vous plaît, j'ai déjà une ou deux autres idées de TP (beaucoup plus longs, par contre) pour la prochaine fois.
Perso, je trouve ça assez sympa, ça nous permet de découvrir certains rouages méconnus du langage (surtout pour ceux qui pratiquent peu comme moi). En revanche, si c'est vraiment long, je ne sais pas...
La seule chose que je me suis amusé à ré-implémenter, c'est les namedtuple, c'était d'ailleurs assez intéressant.
En revanche, si c'est vraiment long, je ne sais pas...
En fait, l'idée que j'ai en tête serait un TP où l'on jouerait avec une méta-classe pour implémenter des classes abstraites (un peu comme le module abc de la bibliothèque standard). Ce qui le rendrait long, ce serait surtout les explications (pour ne perdre personne en route et bien comprendre le principe des méta-classes, et l'intérêt des ABC), plutôt que la quantité de code à produire. On le voit bien dans ce TP : il suffit d'une trentaine de lignes bien tassées pour reproduire les properties, mais le plus dur reste de comprendre le mécanisme qui entre en action derrière.
Le principe des méta-classes, en lui-même, n'est pas extrêmement compliqué (c'est ni plus ni moins qu'une classe qui surcharge sa méthode spéciale __new__), c'est surtout leur portée et la catégorie de problèmes qu'elles permettent de résoudre qui est assez difficile à cerner.
c'est surtout leur portée et la catégorie de problèmes qu'elles permettent de résoudre qui est assez difficile à cerner.
Je le conçois aisément, et trouver un exemple concret où cette utilisation te permet de démontrer son utilité va être difficile.
Comme je l'ai dit : les Abstract Base Classes, ou bien les interfaces, ou bien tout autre genre de classe que l'on peut trouver dans des langages comme Java ou C++ et qui n'existent pas par défaut en Python, sont de bons exemples d'application des métaclasses.
Mais de préférence les Abstract Base Classes puisqu'il s'agit d'un module de la bibliothèque standard (et que c'est aussi l'occasion d'expliquer/de découvrir comment ça fonctionne), et que dans la PEP-3119 , Guido semble montrer qu'il s'agit d'un mécanisme un plus puissant et plus commode à utiliser en Python que les interfaces…
À la réflexion, du point de vue pédagogique les interfaces pourraient même être une étape vers les ABC.
Mais on commence à dériver sérieusement du sujet, là.
[TP][Intermédiaire - Avancé] Un mécanisme de "properties"
× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
× Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.