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 :
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 :
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 :
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__.
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.
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.
>>>
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éé.
>>>
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.
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.
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.
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 :
>>>
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.
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.