IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Composition de programmes

Partie 2 : Construire des abstractions avec des données


précédentsommairesuivant

6. Implémentation de classes et d'objets

Lorsque nous travaillons dans le paradigme de la programmation orientée objet, nous utilisons la métaphore de l'objet pour guider l'organisation de nos programmes. L'essentiel de la façon de représenter et de manipuler les données s'exprime dans les déclarations de classe. Dans cette section, nous voyons que les classes et les objets peuvent eux-mêmes être représentés en utilisant seulement des fonctions et des dictionnaires. Le but de la mise en œuvre d'un système objet de cette manière est d'illustrer que l'utilisation de la métaphore de l'objet ne nécessite pas de langage de programmation spécial. Les programmes peuvent être orientés objet, même dans les langages de programmation qui n'ont pas de système objet intégré.

Pour implémenter des objets, nous abandonnerons la notation pointée (qui nécessite un support de langue intégré), mais créerons des dictionnaires de répartition qui se comporteront de la même manière que les éléments du système objet intégré. Nous avons déjà vu comment implémenter un comportement de passage de message à travers des dictionnaires de distribution. Pour implémenter un système objet dans son intégralité, nous envoyons des messages entre instances, classes et classes parentes, qui sont tous des dictionnaires contenant des attributs.

Nous n'implémenterons pas tout le système d'objets de Python, qui inclut des fonctionnalités que nous n'avons pas couvertes dans ce texte (par exemple, les métaclasses et les méthodes statiques). Nous nous concentrerons plutôt sur des classes définies par l'utilisateur sans héritage multiple et sans comportement introspectif (comme retourner la classe d'une instance). Notre implémentation n'est pas destinée à suivre la spécification précise du système de types de Python. Au lieu de cela, il est conçu pour implémenter la fonctionnalité de base qui permet la métaphore de l'objet.

6-1. Instances

Nous commençons avec des instances. Une instance a des attributs nommés, tels que le solde d'un compte, qui peuvent être définis et récupérés. Nous implémentons une instance utilisant un dictionnaire de distribution qui répond aux messages de types « accesseur » et « mutateur » sur les valeurs d'attribut. Les attributs eux-mêmes sont stockés dans un dictionnaire local appelé attributes.

Comme nous l'avons vu précédemment dans ce tutoriel, les dictionnaires eux-mêmes sont des types de données abstraits. Nous avons implémenté des dictionnaires avec des listes, nous avons implémenté des listes avec des paires, et nous avons implémenté des paires avec des fonctions. Comme nous implémentons un système d'objets en termes de dictionnaires, gardez à l'esprit que nous pourrions tout aussi bien implémenter des objets en n'utilisant que des fonctions.

Pour commencer notre implémentation, nous supposons que nous avons une implémentation de classe qui peut rechercher les noms qui ne font pas partie de l'instance. Nous passons à make_instance une classe sous la forme du paramètre cls :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
>>> def make_instance(cls):
        """Return a new object instance, which is a dispatch dictionary."""
        def get_value(name):
            if name in attributes:
                return attributes[name]
            else:
                value = cls['get'](name)
                return bind_method(value, instance)
        def set_value(name, value):
            attributes[name] = value
        attributes = {}
        instance = {'get': get_value, 'set': set_value}
        return instance

instance est un dictionnaire de distribution qui répond aux messages get et set. Le message set correspond à l'affectation d'attribut dans le système objet de Python : tous les attributs assignés sont stockés directement dans le dictionnaire d'attributs local de l'objet. Dans get, si le nom name n'apparaît pas dans le dictionnaire des attributs locaux attributes, alors il est recherché dans la classe. Si la value retournée par cls est une fonction, elle doit être liée à l'instance.

Valeurs de méthode liées. La fonction get_value dans make_instance trouve un attribut nommé dans sa classe avec get, puis appelle bind_method. La liaison d'une méthode s'applique uniquement aux valeurs de fonction et crée une valeur de méthode liée à partir d'une valeur de fonction en insérant l'occurrence en tant que premier argument :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
>>> def bind_method(value, instance):
        """Return a bound method if value is callable, or value otherwise."""
        if callable(value):
            def method(*args):
                return value(instance, *args)
            return method
        else:
            return value

Quand une méthode est appelée, le premier paramètre self sera lié à la valeur de instance par cette définition.

6-2. Classes

Une classe est aussi un objet, à la fois dans le système objet de Python et dans le système que nous mettons en place ici. Pour simplifier, nous disons que les classes n'ont pas elles-mêmes de classes. (En Python, les classes ont des classes, presque toutes les classes partagent la même classe, appelée type.) Une classe peut répondre à des messages get et set, ainsi qu'au message new :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
>>> def make_class(attributes, base_class=None):
        """Return a new class, which is a dispatch dictionary."""
        def get_value(name):
            if name in attributes:
                return attributes[name]
            elif base_class is not None:
                return base_class['get'](name)
        def set_value(name, value):
            attributes[name] = value
        def new(*args):
            return init_instance(cls, *args)
        cls = {'get': get_value, 'set': set_value, 'new': new}
        return cls

Contrairement à une instance, la fonction get pour les classes n'interroge pas sa classe lorsqu'un attribut est introuvable, mais interroge à la place sa classe parente base_class. Aucune méthode de liaison n'est requise pour les classes.

Initialisation. La fonction new dans make_class appelle init_instance, qui commence par créer une nouvelle instance, puis appelle une méthode appelée __init__.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
>>> def init_instance(cls, *args):
        """Return a new object with type cls, initialized with args."""
        instance = make_instance(cls)
        init = cls['get']('__init__')
        if init:
            init(instance, *args)
        return instance

Cette dernière fonction complète notre système objet. Nous avons maintenant des instances, qui sont mutées (set) localement, mais retombent dans leurs classes sur un accès en lecture (get). Après avoir recherché un nom dans sa classe, une instance se lie aux valeurs de fonction pour créer des méthodes. Enfin, les classes peuvent construire de nouvelles (new) instances, et elles appliquent leur constructeur __init__ immédiatement après la création de l'instance.

Dans ce système d'objet, la seule fonction qui doive être appelée par l'utilisateur est create_class. Toutes les autres fonctionnalités sont activées par le passage de message. De même, le système objet de Python est appelé via l'instruction de class, et toutes ses autres fonctionnalités sont activées via des expressions pointées et des appels aux classes.

6-3. Utiliser des objets implémentés

Revenons maintenant à l'exemple de compte bancaire de la section précédente. En utilisant le système d'objets que nous venons de mettre en place, nous allons créer une classe Account, une classe fille CheckingAccount et une instance de chacune de ces classes.

La classe Account est créée à l'aide d'une fonction create_account_class, dont la structure est similaire à une instruction de class dans Python, mais se termine par un appel à make_class.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
>>> def make_account_class():
        """Return the Account class, which has deposit and withdraw methods."""
        def __init__(self, account_holder):
            self['set']('holder', account_holder)
            self['set']('balance', 0)
        def deposit(self, amount):
            """Increase the account balance by amount and return the new balance."""
            new_balance = self['get']('balance') + amount
            self['set']('balance', new_balance)
            return self['get']('balance')
        def withdraw(self, amount):
            """Decrease the account balance by amount and return the new balance."""
            balance = self['get']('balance')
            if amount > balance:
                return 'Insufficient funds'
            self['set']('balance', balance - amount)
            return self['get']('balance')
        return make_class({'__init__': __init__,
                           'deposit':  deposit,
                           'withdraw': withdraw,
                           'interest': 0.02})

Dans cette fonction, les noms des attributs sont définis à la fin. Contrairement à une déclaration class de Python, qui impose la cohérence entre les noms de fonctions intrinsèques et les noms d'attributs, nous devons spécifier ici la correspondance entre les noms d'attributs et les valeurs manuellement.

La classe Account est finalement instanciée via l'affectation.

 
Sélectionnez
>>> Account = make_account_class()

Ensuite, une instance de compte est créée via le message new, ce qui nécessite un nom pour aller avec le compte nouvellement créé.

 
Sélectionnez
>>> jim_acct = Account['new']('Jim')

Ensuite, des messages get transmis à jim_acct permettent de récupérer les propriétés et les méthodes. Des méthodes peuvent être appelées pour mettre à jour le solde du compte.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
>>> jim_acct['get']('holder')
'Jim'
>>> jim_acct['get']('interest')
0.02
>>> jim_acct['get']('deposit')(20)
20
>>> jim_acct['get']('withdraw')(5)
15

Comme avec le système objet de Python, la définition d'un attribut d'une instance ne modifie pas l'attribut correspondant de sa classe.

 
Sélectionnez
1.
2.
3.
>>> jim_acct['set']('interest', 0.04)
>>> Account['get']('interest')
0.02

Héritage. Nous pouvons créer une classe fille CheckingAccount en surchargeant un sous-ensemble des attributs de la classe. Dans ce cas, nous modifions la méthode withdraw pour percevoir la commission et nous réduisons le taux d'intérêt.

 
Sélectionnez
1.
2.
3.
4.
5.
>>> def make_checking_account_class():
        """Return the CheckingAccount class, which imposes a $1 withdrawal fee."""
        def withdraw(self, amount):
            return Account['get']('withdraw')(self, amount + 1)
        return make_class({'withdraw': withdraw, 'interest': 0.01}, Account)

Dans cette implémentation, nous appelons la fonction withdraw de la classe de base Account de la fonction de withdraw de la sous-classe, comme nous le ferions dans le système d'objets intégré de Python. Nous pouvons créer la sous-classe elle-même et une instance, comme précédemment :

 
Sélectionnez
>>> CheckingAccount = make_checking_account_class()
>>> jack_acct = CheckingAccount['new']('Jack')

Les dépôts se comportent de manière identique, tout comme le constructeur. Les retraits donnent lieu aux frais de 1 $ grâce à la méthode withdraw spécialisée, et interest prend la nouvelle valeur inférieure de CheckingAccount.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
>>> jack_acct['get']('interest')
0.01
>>> jack_acct['get']('deposit')(20)
20
>>> jack_acct['get']('withdraw')(5)
14

Notre système d'objets basé sur des dictionnaires est assez similaire dans sa mise en œuvre au système d'objets intégré de Python. En Python, toute instance d'une classe définie par l'utilisateur a un attribut spécial __dict__ qui stocke les attributs locaux d'instance pour cet objet dans un dictionnaire, un peu comme notre dictionnaire attributes. Python diffère parce qu'il distingue certaines méthodes spéciales qui interagissent avec les fonctions intégrées pour s'assurer que ces fonctions se comportent correctement pour les arguments de nombreux types différents. Les fonctions qui fonctionnent sur différents types font l'objet de la section suivante.


précédentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par John DeNero et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2018 Developpez.com.