5.5. UserDict : une classe enveloppe

Comme vous l'avez vu, FileInfo est une classe qui se comporte comme un dictionnaire. Pour voir ça plus en profondeur, regardons la classe UserDict dans le module UserDict, qui est l'ancêtre de notre classe FileInfo. Cela n'a rien de spécial, la classe est écrite en Python et stockée dans un fichier .py, tout comme notre code. En fait, elle est stockée dans le répertoire lib de votre installation Python.

ASTUCE
Dans l'IDE ActivePython sous Windows, vous pouvez ouvrir rapidement n'importe quel module dans votre chemin de bibliothèques avec File->Locate... (Ctrl-L).

Exemple 5.9. Definition de la classe UserDict


class UserDict:                                1
    def __init__(self, dict=None):             2
        self.data = {}                         3
        if dict is not None: self.update(dict) 4 5
1 Notez que UserDict est une classe de base, elle n'hérite d'aucune classe.
2 Voici la méthode __init__ que nous avons redéfini dans la classe FileInfo. Notez que la liste d'arguments dans cette classe ancêtre est différente de celle du descendant. Cela ne pose pas de problème, chaque classe dérivée peut avoir sa propre liste d'arguments, tant qu'elle appelle la méthode de l'ancêtre avec les arguments corrects. Ici, la classe ancêtre a un moyen de définir des valeurs initiales (en passant un dictionnaire à l'argument dict) ce que notre FileInfo n'exploite pas.
3 Python supporte les données attributs (appelés «variables d'instance» en Java et Powerbuilder, «variables membres» en C++), qui sont des données propres à une instance spécifique de la classe. Dans ce cas, chaque instance de UserDict aura un attribut de données data. Pour référencer cet attribut depuis du code extérieur à la classe, vous devez le qualifier avec le nom de l'instance, instance.data, de la même manière que vous qualifiez une fonction avec son nom de module. Pour référencer un attribut de données depuis la classe, nous utilisons self pour le qualifier. Par convention, tous les données attributs sont initalisées à des valeurs raisonnables dans la méthode __init__. Cependant, ce n'est pas obligatoire, puisque les données attributs, comme les variables locales viennent à existence lorsqu'on leur assigne une valeur pour la première fois.
4 La méthode update est un duplicateur de dictionnaire. Elle copie toutes les clés et valeurs d'un dictionnaire à l'autre. Cela n'efface pas le dictionnaire de destination si il a déjà des clés, celles qui sont présente dans le dictionnaire source seront récrites, mais les autres ne seront pas touchées. Considérez update comme une fonction de fusion, pas de copie.
5 Voici une syntaxe que vous n'avez peut-être pas vu auparavant (je ne l'ai pas employé dans les exemples de ce livre). C'est une instruction if, mais au lieu d'avoir un bloc indenté commençant à la ligne suivante, il y a juste une instruction unique sur la même ligne après les deux points. C'est une syntaxe tout à fait légale, c'est juste un raccourci lorsque vous n'avez qu'une instruction dans un bloc (comme donner une instruction unique sans accolades en C++). Vous pouvez employer cette syntaxe ou vous pouvez avoir du code indenté sur les lignes suivantes, mais vous ne pouvez pas mélanger les deux dans le même bloc.
NOTE
Java et Powerbuilder supportent la surcharge de fonction par liste d'arguments : une classe peut avoir différentes méthodes avec le même nom mais avec un nombre différent d'arguments ou des arguments de type différent. D'autres langages (notamment PL/SQL) supportent même la surcharge de fonction par nom d'argument : une classe peut avoir différentes méthodes avec le même nom et le même nombre d'arguments du même type mais avec des noms d'arguments différents. Python ne supporte ni l'une ni l'autre, il n'a tout simplement aucune forme de surcharge de fonction. Les méthodes sont définies uniquement par leur nom et il ne peut y avoir qu'une méthode par classe avec le même nom. Donc si une classe descendante a une méthode __init__, elle redéfinit toujours la méthode __init__ de la classe ancêtre, même si la descendante la définit avec une liste d'arguments différente. Et la même règle s'applique pour toutes les autres méthodes.
NOTE
Guido, l'auteur originel de Python, explique la redéfinition de méthode de cette manière : «Les classes dérivées peuvent redéfinir les méthodes de leur classes de base. Puisque les méthodes n'ont pas de privilèges spéciaux lorsqu'elles appellent d'autres méthodes du même objet, une méthode d'une classe de base qui appelle une autre méthode définie dans cette même classe de base peut en fait se retrouver à appeler une méthode d'une classe dérivée qui la redéfini (pour les programmeurs C++ cela veut dire qu'en Python toutes les méthodes sont virtuelles).» Si cela n'a pas de sens pour vous (personellement, je m'y perd complètement) vous pouvez ignorer la question. Je me suis juste dit que je ferais circuler l'information.
Attention
Assignez toujours une valeur initiale à toutes les données attributs d'une instance dans la méthode __init__. Cela vous épargera des heures de débogage plus tard, à la poursuite d'exceptions AttributeError pour cause de référence à des attributs non-initialisés (et donc non-existants).

Exemple 5.10. Méthodes ordinaires de UserDict

    def clear(self): self.data.clear()          1
    def copy(self):                             2
        if self.__class__ is UserDict:          3
            return UserDict(self.data)         
        import copy                             4
        return copy.copy(self)                 
    def keys(self): return self.data.keys()     5
    def items(self): return self.data.items()  
    def values(self): return self.data.values()
1 clear est une méthode de classe ordinaire, elle est disponible publiquement et peut être appelée par n'importe qui. Notez que clear, comme toutes les méthodes de classe, a pour premier argument self (rappelez-vous que vous ne mentionnez pas self lorsque vous appelez la méthode, Python l'ajoute pour vous). Notez aussi la technique de base employé par cette classe enveloppe : utiliser un véritable dictionnaire (data) comme données attributs, définir toutes les méthodes d'un véritable dictionnaire et rediriger chaque méthode vers la méthode du véritable dictionnaire (au cas où vous l'auriez oublié, la méthode clear d'un dictionnaire supprime toutes ses clés et leurs valeurs associées).
2 La méthode copy d'un véritable dictionnaire retourne un nouveau dictionnaire qui est un double exact de l'original (avec les mêmes paires clé-valeur). Mais UserDict ne peut pas simplement rediriger la méthode vers self.data.copy, car cette méthode retourne un véritable dictionnaire, alors que nous voulons retourner une nouvelle instance qui soit de la même classe que self.
3 Nous utilisons l'attribut __class__ pour voir si self est un UserDict et, dans ce cas, tout va bien puisque nous savons comment copier un UserDict : il suffit de créer un nouveau UserDict et de lui passer le dictionnaire véritable qu'est self.data.
4 Si self.__class__ n'est pas un UserDict, alors self doit être une classe dérivée de UserDict (par exemple FileInfo), dans ce cas c'est plus compliqué. UserDict ne sait pas comment faire une copie exacte d'un de ses descendants. Il pourrait y avoir, par exemple, d'autres données attributs définies dans la classe dérivée, ce qui nécessiterait de les copier tous. Heureusement Python est fourni avec un module qui remplit cette tâche, le module copy. Je ne vais pas entrer ici dans les détails (bien que ce soit très intéressant et vaille la peine que vous y jetiez un coup d'œil). Il suffit de dire que copy peut copier un objet Python quelconque et que c'est comme cela que nous l'employons ici.
5 Le reste des méthodes est sans difficulté, les appels sont redirigés vers les méthodes de self.data.
NOTE
Dans les versions de Python antérieures à la 2.2, vous ne pouviez pas directement dériver les types de données prédéfinis comme les chaînes, les listes et les dictionnaires. Pour compenser cela, Python est fourni avec des classes enveloppes qui reproduisent le comportement de ces types de données prédéfinis : UserString, UserList et UserDict. En utilisant un mélange de méthodes ordinaires et spéciales, la classe UserDict fait une excellente imitation d'un dictionnaire, mais c'est juste une classe comme les autres, vous pouvez donc la dériver pour créer des classes personalisées semblables à un dictionnaire comme FileInfo. En Python 2.2 et suivant, vous pourriez récrire l'exemple de ce chapitre de manière à ce que FileInfo hérite directement de dict au lieu de UserDict. Cependant, vous devriez quand même lire l'explication du fonctionnement de UserDict au cas où vous auriez besoin d'implémenter ce genre d'objet enveloppe ou au cas où vous auriez à travailler avec une version de Python antérieure à la 2.2.

En Python il est possible de dériver une classe directement du type de données prédéfini dict, comme dans l'exemple suivant. Il y a trois différence avec la version dérivée de UserDict.

Exemple 5.11. Dériver une classe directement du type prédéfini dict


class FileInfo(dict):                  1
    "store file metadata"
    def __init__(self, filename=None): 2
        self["name"] = filename
1 La première différence est que nous n'avons pas besoin d'importer le module UserDict, puisque dict est un type prédéfini et donc toujours disponible. La seconde est que nous dérivons notre classe de dict directement et non de UserDict.UserDict.
2 La troisième différence est subtile mais importante. À cause de la manière dont UserDict fonctionne en interne, nous devons appeler explicitement sa méthode __init__ pour l'initialiser correctement. dict ne fonctionne pas de la même manière, ce n'est pas une enveloppe et il ne demande pas d'initialisation explicite.

Pour en savoir plus sur UserDict