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

5. Programmation orientée objet

La programmation orientée objet (POO) est une méthode d'organisation de programmes qui rassemble bon nombre des idées présentées dans ce tutoriel. Comme les fonctions d'abstraction des données, les classes créent des barrières d'abstraction entre l'utilisation et la mise en œuvre des données. Comme les dictionnaires ou tables de distribution, les objets répondent à des demandes comportementales. Comme les structures de données mutables, les objets ont un état local qui n'est pas directement accessible depuis l'environnement global. Le système d'objets de Python fournit une syntaxe pratique pour promouvoir l'utilisation de ces techniques pour l'organisation de programmes. Une grande partie de cette syntaxe est commune à d'autres langages de programmation orientés objet.

Le système d'objet offre plus que de la commodité. Il permet une nouvelle métaphore pour concevoir des programmes dans lesquels plusieurs agents indépendants interagissent au sein de l'ordinateur. Chaque objet regroupe un état local et un comportement d'une manière qui résume la complexité des deux. Les objets communiquent entre eux et les résultats utiles sont calculés à la suite de leur interaction. Non seulement les objets transmettent-ils des messages, mais ils partagent également un comportement avec d'autres objets du même type et héritent des caractéristiques des types apparentés.

Le paradigme de la programmation orientée objet a son propre vocabulaire qui prend en charge la métaphore de l'objet. Nous avons vu qu'un objet est une donnée qui a des méthodes et des attributs, accessibles par notation pointée. Chaque objet a également un type, appelé sa classe. Pour créer de nouveaux types de données, nous implémentons de nouvelles classes.

5-1. Objets et classes

Une classe sert de modèle pour tous les objets dont le type est cette classe. Chaque objet est une instance d'une classe particulière. Les objets que nous avons utilisés jusqu'ici ont tous des classes intégrées au langage, mais il est également possible de créer de nouvelles classes définies par l'utilisateur. Une définition de classe spécifie les attributs et les méthodes partagés entre les objets de cette classe. Nous présenterons la définition d'une classe en revisitant l'exemple d'un compte bancaire.

Lors de l'introduction de l'état local, nous avons vu que les comptes bancaires sont naturellement modélisés comme des valeurs mutables qui ont un solde (balance). Un objet compte bancaire doit avoir une méthode de retrait (withdraw) qui met à jour le solde du compte et renvoie le montant demandé, s'il est disponible. Pour compléter l'abstraction : un compte bancaire doit être en mesure de renvoyer son solde (balance) courant et le nom du titulaire (holder) du compte, et permettre le dépôt (deposit) d'un montant.

Une classe Account nous permet de créer plusieurs instances de comptes bancaires. L'acte de créer une nouvelle instance d'objet s'appelle une instanciation de la classe. La syntaxe en Python pour instancier une classe est identique à la syntaxe d'appel d'une fonction. Dans ce cas, nous appelons Account avec l'argument 'Kirk', le nom du titulaire du compte.

 
Sélectionnez
>>> a = Account('Kirk')

Un attribut d'un objet est une paire nom-valeur associée à l'objet, accessible par notation pointée. Les attributs spécifiques à un objet particulier, par opposition aux attributs communs à tous les objets d'une classe, sont appelés attributs d'instance. Chaque compte a son propre solde et son propre nom de compte ; ce sont des exemples d'attributs d'instance. Dans la communauté de programmation plus large, les attributs d'instance peuvent également s'appeler champs, propriétés ou variables d'instance.

 
Sélectionnez
1.
2.
3.
4.
>>> a.holder
'Kirk'
>>> a.balance
0

Les fonctions qui s'appliquent à un objet ou effectuent des calculs spécifiques à un objet s'appellent des méthodes. Les valeurs de retour d'une méthode peuvent dépendre des attributs de l'objet ; une méthode peut aussi avoir pour effet de modifier un ou plusieurs attributs d'un objet. Par exemple, deposit est une méthode de notre objet Account a. Elle prend un argument, le montant à déposer, modifie l'attribut solde (balance) de l'objet et renvoie le solde résultant.

 
Sélectionnez
>>> a.deposit(15)
15

Nous disons que les méthodes sont invoquées sur un objet particulier. Suite à l'invocation de la méthode withdraw, soit le retrait est approuvé et le montant est déduit du solde, soit la demande est refusée et la méthode renvoie un message d'erreur.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
>>> a.withdraw(10)  # The withdraw method returns the balance after withdrawal
5
>>> a.balance       # The balance attribute has changed
5
>>> a.withdraw(10)
'Insufficient funds'

Comme on le voit ci-dessus, le comportement d'une méthode peut dépendre des attributs changeants de l'objet. Deux appels successifs à withdraw avec le même argument renvoient des résultats différents.

5-2. Définition de classes

Les classes définies par l'utilisateur sont créées par des déclarations class, constituées d'une seule clause. Une déclaration de classe définit le nom de la classe, puis inclut une suite d'instructions pour définir les attributs de la classe :

 
Sélectionnez
1.
2.
class <name>:
    <suite>

Lorsqu'une déclaration de classe est exécutée, une nouvelle classe est créée et liée à <name> dans le premier cadre de l'environnement courant. La suite est ensuite exécutée. Tout nom lié dans la <suite> d'une déclaration class, via des instructions def ou des affectations, crée ou modifie des attributs de la classe.

Les classes sont généralement organisées autour de la manipulation des attributs d'instance, qui sont les paires nom-valeur associées à chaque instance de cette classe. La classe spécifie les attributs d'instance de ses objets en définissant une méthode d'initialisation de nouveaux objets. Par exemple, une partie de l'initialisation d'un objet de la classe Account consiste à lui affecter un solde de départ de 0.

La <suite> d'une déclaration de class contient des instructions def qui définissent de nouvelles méthodes pour les objets de cette classe. La méthode qui initialise les objets a un nom spécial dans Python, __init__ (deux caractères de soulignement de chaque côté du mot « init »), et s'appelle le constructeur de la classe.

 
Sélectionnez
1.
2.
3.
4.
>>> class Account:
        def __init__(self, account_holder):
            self.balance = 0
            self.holder = account_holder

La méthode __init__ de la classe Account a deux paramètres formels. Le premier, self, est lié à l'objet Account nouvellement créé. Le second paramètre, account_holder (titulaire du compte), est lié à l'argument passé à la classe lorsqu'elle est appelée à être instanciée.

Le constructeur lie l'attribut d'instance balance à 0. Il lie également le nom d'attribut holder à la valeur du nom account_holder. Le paramètre formel account_holder est un nom local dans la méthode __init__. En revanche, le nom holder lié via l'instruction d'affectation finale est persistant, car il est stocké en tant qu'attribut de self, via la notation pointée.

Après avoir défini la classe Account, nous pouvons l'instancier :

 
Sélectionnez
>>> a = Account('Kirk')

Cet « appel » à la classe Account crée un nouvel objet qui est une instance d'Account, puis appelle la fonction constructeur __init__ avec deux arguments : l'objet nouvellement créé et la chaîne de caractères 'Kirk'. Par convention, nous utilisons le nom de paramètre self pour le premier argument d'un constructeur, car il est lié à l'objet instancié. Cette convention est adoptée dans pratiquement tous les programmes Python.

Maintenant, nous pouvons accéder au solde et au titulaire de l'objet a en utilisant la notation pointée :

 
Sélectionnez
1.
2.
3.
4.
>>> a.balance
0
>>> a.holder
'Kirk'

Identité. Chaque nouvelle instance de compte a son propre attribut de solde, dont la valeur est indépendante des autres objets de la même classe.

 
Sélectionnez
1.
2.
3.
4.
>>> b = Account('Spock')
>>> b.balance = 200
>>> [acc.balance for acc in (a, b)]
[0, 200]

Pour instaurer cette séparation, chaque objet instance d'une classe définie par l'utilisateur a une identité unique. L'identité de l'objet est comparée en utilisant les opérateurs is et not :

 
Sélectionnez
>>> a is a
True
>>> a is not b
True

Bien qu'ils soient construits à partir d'appels identiques, les objets liés à a et b ne sont pas identiques. Comme d'habitude, lier un objet à un nouveau nom en utilisant l'assignation ne crée pas un nouvel objet.

 
Sélectionnez
>>> c = a
>>> c is a
True

Les nouveaux objets dont les classes sont définies par l'utilisateur sont créés uniquement lorsqu'une classe (comme Account) est instanciée avec une syntaxe d'appel d'expression.

Méthodes. Les méthodes sont définies (comme les fonctions) par une instruction def dans la suite d'une déclaration de type class. Ci-dessous, deposit et withdraw sont définies comme des méthodes sur les objets de la classe Account.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
>>> class Account:
        def __init__(self, account_holder):
            self.balance = 0
            self.holder = account_holder
        def deposit(self, amount):
            self.balance = self.balance + amount
            return self.balance
        def withdraw(self, amount):
            if amount > self.balance:
                return 'Insufficient funds'
            self.balance = self.balance - amount
            return self.balance

Bien que les définitions de méthodes ne diffèrent pas des définitions de fonctions dans la façon dont elles sont déclarées, les définitions de méthodes ont un effet différent lorsqu'elles sont exécutées. La valeur de fonction créée par une instruction def dans une déclaration class est liée au nom déclaré, mais liée localement dans la classe en tant qu'attribut. Cette valeur est invoquée comme une méthode utilisant la notation pointée depuis une instance de la classe.

Comme nous l'avons déjà vu, chaque définition de méthode inclut un premier paramètre spécial self, qui est lié à l'objet sur lequel la méthode est invoquée. Par exemple, supposons que la méthode deposit soit invoquée sur un objet particulier de la classe Account et qu'elle ne passe qu'un seul argument : le montant déposé. L'objet lui-même est lié à self, tandis que l'argument est lié à amount (le montant du dépôt). Toutes les méthodes invoquées ont accès à l'objet via le paramètre self, et peuvent ainsi toutes accéder et manipuler l'état de l'objet.

Pour invoquer ces méthodes, nous utilisons de nouveau la notation pointée, comme on le voit ci-dessous :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
>>> spock_account = Account('Spock')
>>> spock_account.deposit(100)
100
>>> spock_account.withdraw(90)
10
>>> spock_account.withdraw(90)
'Insufficient funds'
>>> spock_account.holder
'Spock'

Lorsqu'une méthode est invoquée via la notation pointée, l'objet lui-même (lié à spock_account, dans ce cas) joue un double rôle. D'abord, il détermine ce que le nom withdraw signifie ; withdraw n'est pas un nom dans l'environnement, mais un nom qui est local dans la classe Account. Deuxièmement, il est lié au premier paramètre self lorsque la méthode de withdraw est invoquée.

5-3. Passage de message et expressions pointées

Les méthodes, qui sont définies dans les classes, et les attributs d'instance, qui sont généralement alimentés dans les constructeurs, sont les éléments fondamentaux de la programmation orientée objet. Ces deux concepts répliquent une grande partie du comportement d'un dictionnaire de distribution dans un message transmettant l'implémentation d'une valeur de données. Les objets prennent des messages en utilisant la notation pointée, mais au lieu que ces messages soient des clefs d'un dictionnaire, ils sont des noms locaux à une classe. Les objets ont également des valeurs d'état local nommées (les attributs d'instance), mais cet état peut être accédé et manipulé en utilisant la notation pointée, sans avoir à employer d'instructions nonlocal dans l'implémentation.

L'idée centrale dans le passage des messages était que les valeurs de données devaient avoir un comportement en répondant aux messages qui sont pertinents pour le type abstrait qu'ils représentent. La notation pointée est une caractéristique syntaxique de Python qui formalise la métaphore du passage du message. L'avantage d'utiliser un langage avec un système objet intégré est que le passage de message peut interagir de manière transparente avec d'autres fonctionnalités de langage, telles que des instructions d'affectation. Nous n'avons pas besoin de messages différents pour nos « accesseurs » et nos « mutateurs » permettant d'interagir avec la valeur associée à un nom d'attribut local ; la syntaxe du langage nous permet d'utiliser directement le nom du message.

Expressions pointées. Le fragment de code spock_account.deposit s'appelle une expression pointée. Une expression pointée consiste en une expression, un point et un nom :

 
Sélectionnez
<expression> . <name>

L'<expression> peut être une expression Python valide quelconque, mais <name> doit être un nom simple (pas une expression qui s'évalue à un nom). Une expression pointée s'évalue à la valeur de l'attribut ayant le nom (<name>) donné, pour l'objet qui est la valeur de l'<expression>.

La fonction intégrée au langage getattr renvoie également un attribut pour un objet par son nom. C'est une fonction équivalente à la notation pointée. En utilisant getattr, nous pouvons rechercher un attribut en utilisant une chaîne, comme nous l'avons fait avec un dictionnaire de distribution.

 
Sélectionnez
>>> getattr(spock_account, 'balance')
10

Nous pouvons également tester si un objet a un attribut nommé avec hasattr :

 
Sélectionnez
>>> hasattr(spock_account, 'deposit')
True

Les attributs d'un objet incluent tous ses attributs d'instance, ainsi que tous les attributs (y compris les méthodes) définis dans sa classe. Les méthodes sont des attributs de la classe qui nécessitent un traitement spécial.

Méthodes et fonctions. Lorsqu'une méthode est appelée sur un objet, cet objet est implicitement transmis en tant que premier argument de la méthode. C'est-à-dire que l'objet qui représente la valeur d'<expression> à la gauche du point est passé automatiquement en tant que premier argument de la méthode nommée du côté droit de l'expression pointée. Il en résulte que l'objet est lié au paramètre self.

Pour réaliser cette liaison automatique avec self, Python distingue les fonctions, que nous avons créées depuis le début de ce livre, et les méthodes liées, qui associent une fonction et l'objet sur lequel cette méthode sera appelée. Une valeur de méthode liée est déjà associée à son premier argument, l'instance sur laquelle elle a été invoquée, qui sera appelée self lors de l'appel de la méthode.

Nous pouvons voir la différence dans l'interpréteur interactif en appelant type sur les valeurs renvoyées par des expressions pointées. En tant qu'attribut d'une classe, une méthode est juste une fonction, mais en tant qu'attribut d'une instance, c'est une méthode liée :

 
Sélectionnez
>>> type(Account.deposit)
<class 'function'>
>>> type(spock_account.deposit)
<class 'method'>

Ces deux résultats ne diffèrent que par le fait que le premier est une fonction standard à deux arguments avec des paramètres self et amount. La seconde est une méthode à un argument, où le nom self sera automatiquement lié à l'objet spock_account lorsque la méthode sera appelée, tandis que le paramètre sera lié à l'argument passé à la méthode. Ces deux valeurs, qu'il s'agisse de valeurs de fonction ou de valeurs de méthode liées, sont associées au même corps de la fonction deposit.

Nous pouvons appeler deposit de deux façons : en tant que fonction et en tant que méthode liée. Dans le premier cas, nous devons explicitement fournir un argument pour le paramètre self. Dans le second cas, le paramètre self est automatiquement lié.

 
Sélectionnez
1.
2.
3.
4.
>>> Account.deposit(tom_account, 1001)  # The deposit function requires 2 arguments
1011
>>> tom_account.deposit(1000)           # The deposit method takes 1 argument
2011

La fonction getattr se comporte exactement comme la notation pointée : si son premier argument est un objet mais que le nom est une méthode définie dans la classe, alors getattr renvoie une valeur de méthode liée. D'un autre côté, si le premier argument est une classe, alors getattr renvoie directement la valeur de l'attribut, qui est une fonction simple.

Guide pratique : conventions de nommage. Les noms de classes sont classiquement écrits en utilisant la convention CapWords (également appelée CamelCase parce que les majuscules au milieu d'un nom suggèrent les bosses d'un chameau). Les noms de méthode suivent la convention standard de nommage des fonctions et utilisent des mots en minuscules séparés par des caractères de soulignement.

Dans certains cas, il existe des variables et des méthodes d'instance liées à la maintenance et à la cohérence d'un objet que nous ne voulons pas que les utilisateurs de l'objet voient ou utilisent. Elles ne font pas partie de l'abstraction définie par une classe, mais font plutôt partie de l'implémentation. La convention de Python stipule que si un nom d'attribut commence par un trait de soulignement, il ne doit être employé que dans les méthodes de la classe elle-même, mais non par les utilisateurs de la classe.

5-4. Attributs de classe

Certains attributs sont partagés entre tous les objets d'une classe donnée. Ces attributs sont associés à la classe elle-même, plutôt qu'à une quelconque instance individuelle de la classe. Par exemple, supposons qu'une banque paie des intérêts sur le solde des comptes à un taux d'intérêt fixe. Ce taux d'intérêt peut changer, mais il s'agit d'une valeur unique partagée pour tous les comptes.

Les attributs de classe sont créés par des instructions d'affectation dans la suite d'une déclaration de class, en dehors de toute définition de méthode. Dans la communauté de développeurs plus large, les attributs de classe peuvent aussi s'appeler variables de classe ou variables statiques. La déclaration de classe suivante crée un attribut de classe nommé interest pour la classe Account :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
>>> class Account(object):
        interest = 0.02            # A class attribute
        def __init__(self, account_holder):
            self.balance = 0
            self.holder = account_holder
        # Additional methods would be defined here

Cet attribut est toujours accessible depuis n'importe quelle instance de la classe.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
>>> tom_account = Account('Tom')
>>> jim_account = Account('Jim')
>>> tom_account.interest
0.02
>>> jim_account.interest
0.02

Toutefois, une instruction d'affectation sur un attribut de classe modifie la valeur de l'attribut pour toutes les instances de la classe.

 
Sélectionnez
1.
2.
3.
4.
5.
>>> Account.interest = 0.04
>>> tom_account.interest
0.04
>>> jim_account.interest
0.04

Noms d'attribut. Nous avons introduit suffisamment de complexité dans notre système d'objets pour que nous devions spécifier comment les noms se résolvent en des attributs particuliers. Après tout, nous pourrions facilement avoir un attribut de classe et un attribut d'instance ayant le même nom.

Comme nous l'avons vu, une expression pointée consiste en une expression, un point et un nom :

 
Sélectionnez
<expression> .  <name>

Pour évaluer une expression pointée :

  1. Évaluez l'<expression> à gauche du point, ce qui donne l'objet de l'expression pointée.
  2. <name> est comparé aux attributs d'instance de cet objet ; si un attribut d'instance portant ce nom existe, sa valeur est renvoyée.
  3. Si <name> n'apparaît pas parmi les attributs d'instance, alors <name> est recherché dans la classe, ce qui donne une valeur d'attribut de classe.
  4. Cette valeur est renvoyée sauf s'il s'agit d'une fonction, auquel cas une méthode liée est renvoyée à la place.

Dans cette procédure d'évaluation, les attributs d'instance sont détectés avant les attributs de classe, de même que les noms locaux ont priorité sur les noms globaux dans un environnement. Les méthodes définies dans la classe sont combinées avec l'objet de l'expression pointée pour former une méthode liée pendant la quatrième étape de cette procédure d'évaluation. La procédure de recherche d'un nom dans une classe a des nuances supplémentaires qui apparaîtront sous peu, quand nous aurons introduit la notion d'héritage de classe.

Affectation d'attribut. Toutes les instructions d'affectation qui contiennent une expression pointée du côté gauche de l'affectation modifient les attributs de l'objet de cette expression pointée. Si l'objet est une instance, l'affectation définit un attribut d'instance. Si l'objet est une classe, alors l'affectation définit un attribut de classe. En conséquence de cette règle, l'affectation à un attribut d'un objet ne peut pas affecter les attributs de sa classe. Les exemples ci-dessous illustrent cette distinction.

Si nous affectons à l'attribut d'intérêt d'une instance de compte, nous créons un nouvel attribut d'instance qui a le même nom que l'attribut de classe existant.

 
Sélectionnez
>>> jim_account.interest = 0.08

et cette valeur d'attribut sera renvoyée par une expression pointée :

 
Sélectionnez
>>> jim_account.interest
0.08

Toutefois, l'attribut de classe interest conserve sa valeur d'origine, qui est renvoyée pour tous les autres comptes :

 
Sélectionnez
>>> tom_account.interest
0.04

Les modifications apportées à l'attribut de classe affecteront tom_account, mais l'attribut d'instance de jim_account ne sera pas affecté.

 
Sélectionnez
1.
2.
3.
4.
5.
>>> Account.interest = 0.05  # changing the class attribute
>>> tom_account.interest     # changes instances without like-named instance attributes
0.05
>>> jim_account.interest     # but the existing instance attribute is unaffected
0.08

5-5. Héritage

Lorsque nous travaillons dans le paradigme de la programmation orientée objet, nous constatons souvent que différents types de données abstraites sont liés. En particulier, nous trouvons que les classes similaires diffèrent dans leur degré de spécialisation. Deux classes peuvent avoir des attributs similaires, mais l'une représente un cas particulier de l'autre.

Par exemple, nous souhaitons implémenter un compte-chèques (CheckingAccount), différent d'un compte standard. Un compte-chèques facture une commission égale à 1 $ pour chaque retrait et offre un taux d'intérêt plus bas. Voici le comportement souhaité :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
>>> ch = CheckingAccount('Tom')
>>> ch.interest     # Lower interest rate for checking accounts
0.01
>>> ch.deposit(20)  # Deposits are the same
20
>>> ch.withdraw(5)  # withdrawals decrease balance by an extra charge
14

Un CheckingAccount est une spécialisation d'un Account. Dans la terminologie OOP, le compte générique servira de classe de base de CheckingAccount, alors que CheckingAccount sera une sous-classe de Account. (Les termes classe parente, classe mère et superclasse sont également utilisés pour la classe de base, tandis que les termes classe enfant ou classe fille sont également employés pour la sous-classe.)

Une sous-classe hérite des attributs de sa classe de base, mais peut remplacer certains attributs, y compris certaines méthodes. Avec l'héritage, nous spécifions seulement ce qui est différent entre la sous-classe et la classe de base. Tout ce que nous laissons non spécifié dans la sous-classe est automatiquement supposé se comporter exactement comme la classe de base.

L'héritage a aussi un rôle dans notre métaphore de l'objet, en plus d'être une caractéristique organisationnelle utile. L'héritage est censé représenter des relations de type est-un ou est-une (is-a) entre les classes, par opposition aux relations a-un (has-a). Un compte-chèques est-un type de compte spécifique. Avoir un CheckingAccount qui hérite de Account est donc une utilisation appropriée de l'héritage. D'un autre côté, une banque a une liste de comptes bancaires qu'elle gère, donc la liste de compte ne doit pas hériter de la banque (ni l'inverse). Au lieu de cela, une liste d'objets de compte s'exprimerait naturellement sous la forme d'attribut d'instance d'un objet banque.

5-6. Utilisation de l'héritage

Nous spécifions l'héritage en plaçant la classe parente entre parenthèses dans la déclaration de la classe fille.

Pour commencer, nous donnons une implémentation complète de la classe Account, qui inclut les docstrings pour la classe et ses méthodes.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
>>> class Account(object):
        """Un compte bancaire qui a un solde non négatif."""
        interest = 0.02
        def __init__(self, account_holder):
            self.balance = 0
            self.holder = account_holder
        def deposit(self, amount):
            """Augmenter le solde du compte et retourner le nouveau solde."""
            self.balance = self.balance + amount
            return self.balance
        def withdraw(self, amount):
            """Diminuer le solde du compte et retourner le nouveau solde."""
            if amount > self.balance:
                return 'Insufficient funds'
            self.balance = self.balance - amount
            return self.balance

Une implémentation complète de CheckingAccount figure ci-dessous.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
>>> class CheckingAccount(Account):
        """Un compte bancaire qui facture les retraits."""
        withdraw_charge = 1
        interest = 0.01
        def withdraw(self, amount):
            return Account.withdraw(self, amount + self.withdraw_charge)

Ici, nous introduisons un attribut de classe withdraw_charge spécifique à la classe CheckingAccount. Nous affectons une valeur inférieure à l'attribut interest. Nous définissons également une nouvelle méthode de retrait withdraw pour remplacer le comportement défini dans la classe Account. En l'absence d'autres instructions dans la suite de classes, tous les autres comportements sont hérités de la classe de base Account.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
>>> checking = CheckingAccount('Sam')
>>> checking.deposit(10)
10
>>> checking.withdraw(5)
4
>>> checking.interest
0.01

L'expression checking.deposit évalue une méthode liée à la création de dépôts, qui a été définie dans la classe Account. Lorsque Python résout un nom dans une expression pointée qui n'est pas un attribut de l'instance, il recherche le nom dans la classe. En fait, le fait de « rechercher » un nom dans une classe essaie de trouver ce nom dans chaque classe parente de la chaîne d'héritage de la classe de l'objet d'origine. Nous pouvons définir cette procédure de manière récursive. Pour rechercher un nom dans une classe :

  1. S'il correspond à un attribut dans la classe, renvoyez la valeur de l'attribut.
  2. Sinon, recherchez le nom dans la classe parente, s'il y en a une.

Dans le cas de deposit, Python aurait recherché le nom en premier sur l'instance, puis dans la classe CheckingAccount. Enfin, il apparaîtrait dans la classe Account, où deposit est défini. Selon notre règle d'évaluation pour les expressions pointées, puisque deposit est une fonction recherchée dans la classe pour l'instance checking, l'expression pointée est évaluée en une valeur de méthode liée. Cette méthode est invoquée avec l'argument 10, qui appelle la méthode de dépôt avec self lié à l'objet de checking et le montant amount lié à 10.

La classe d'un objet reste constante tout au long de ce processus. Même si la méthode deposit a été trouvée dans la classe Account, deposit est appelée avec self lié à une instance de CheckingAccount, et non d'Account.

Appel des ancêtres. Les attributs qui ont été remplacés sont toujours accessibles via les objets de classe. Par exemple, nous avons implémenté la méthode de retrait de CheckingAccount en appelant la méthode withdraw de Account avec un argument qui incluait la withdraw_charge.

Notez que nous avons appelé self.withdraw_charge plutôt que CheckingAccount.withdraw_charge qui serait équivalent. L'avantage de la première solution par rapport à la seconde est qu'une classe qui hérite de CheckingAccount peut modifier la commission (withdraw_charge). Si tel est le cas, nous aimerions que notre nouvelle classe trouve cette nouvelle valeur plutôt que l'ancienne.

5-7. Héritage multiple

Python prend en charge le concept d'une sous-classe héritant des attributs de plusieurs classes de base, une fonctionnalité de langage appelée héritage multiple.

Supposons que nous ayons un compte d'épargne (SavingsAccount) qui hérite d'Account, mais facture des frais minimes aux clients chaque fois qu'ils effectuent un dépôt.

 
Sélectionnez
1.
2.
3.
4.
>>> class SavingsAccount(Account):
        deposit_charge = 2
        def deposit(self, amount):
            return Account.deposit(self, amount - self.deposit_charge)

Ensuite, un as du marketing imagine un compte « vu à la télé », AsSeenOnTVAccount, combinant les meilleures caractéristiques de CheckingAccount et de SavingsAccount : frais de retrait, frais de dépôt et un taux d'intérêt bas. C'est à la fois un compte-chèques et un compte d'épargne, tout-en-un ! « Si nous le faisons », affirme notre gourou, « quelqu'un va s'inscrire et payer tous ces frais, nous allons même leur donner un dollar. »

 
Sélectionnez
1.
2.
3.
4.
>>> class AsSeenOnTVAccount(CheckingAccount, SavingsAccount):
        def __init__(self, account_holder):
            self.holder = account_holder
            self.balance = 1           # A free dollar!

En fait, cette implémentation est complète. Les retraits et les dépôts génèrent des frais, en utilisant respectivement les définitions de fonction dans CheckingAccount et SavingsAccount.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
>>> such_a_deal = AsSeenOnTVAccount("John")
>>> such_a_deal.balance
1
>>> such_a_deal.deposit(20)            # $2 fee from SavingsAccount.deposit
19
>>> such_a_deal.withdraw(5)            # $1 fee from CheckingAccount.withdraw
13

Les références non ambiguës sont résolues correctement comme prévu :

 
Sélectionnez
>>> such_a_deal.deposit_charge
2
>>> such_a_deal.withdraw_charge
1

Mais qu'en est-il lorsque la référence est ambiguë, comme la référence à la méthode withdraw définie dans Account et dans CheckingAccount ? La figure ci-dessous illustre un graphe d'héritage pour la classe AsSeenOnTVAccount. Chaque flèche pointe d'une sous-classe vers une classe de base.

Image non disponible

Pour une forme « en diamant » simple comme celle-ci, Python résout les noms de gauche à droite, puis vers le haut. Dans cet exemple, Python recherche un nom d'attribut dans les classes suivantes, dans l'ordre, jusqu'à ce qu'un attribut portant ce nom soit trouvé :

 
Sélectionnez
AsSeenOnTVAccount, CheckingAccount, SavingsAccount, Account, object

Il n'y a pas de solution correcte au problème de l'ordre des héritages, car il y a des cas dans lesquels nous préférons donner la priorité à certaines classes héritées plutôt qu'à d'autres. Cependant, tout langage de programmation prenant en charge l'héritage multiple doit sélectionner un ordre d'une manière cohérente, afin que les utilisateurs du langage puissent prédire le comportement de leurs programmes.

Lectures complémentaires. Python résout ce nom en utilisant un algorithme récursif appelé C3 Method Resolution Ordering. L'ordre de résolution de méthode de n'importe quelle classe peut être interrogé en utilisant la méthode mro sur toutes les classes.

 
Sélectionnez
>>> [c.__name__ for c in AsSeenOnTVAccount.mro()]
['AsSeenOnTVAccount', 'CheckingAccount', 'SavingsAccount', 'Account', 'object']

L'algorithme précis pour trouver l'ordre de résolution de méthode dépasse le cadre du présent texte, mais est décrit par l'auteur principal de Python avec une référence à l'article original.

5-8. Le rôle des objets

Le système objet Python est conçu pour rendre l'abstraction des données et le passage des messages à la fois pratique et flexible. La syntaxe spécialisée des classes, des méthodes, de l'héritage et des expressions pointées nous permet de formaliser la métaphore de l'objet dans nos programmes, ce qui améliore notre capacité à organiser de grands programmes.

En particulier, nous aimerions que notre système d'objets favorise une séparation des préoccupations entre les différents aspects du programme. Chaque objet dans un programme encapsule et gère une partie de l'état du programme, et chaque déclaration de classe définit les fonctions qui implémentent une partie de la logique globale du programme. Les barrières d'abstraction imposent des frontières entre les différents aspects d'un grand programme.

La programmation orientée objet est particulièrement bien adaptée aux programmes qui modélisent des systèmes ayant des parties séparées mais en interaction. Par exemple, différents utilisateurs interagissent dans un réseau social, différents personnages interagissent dans un jeu et différentes formes interagissent dans une simulation physique. Quand ils représentent ce genre de systèmes, les objets d'un programme modélisent souvent naturellement les objets du système modélisé, et les classes représentent leurs types et leurs relations.

D'un autre côté, il se peut que les classes ne fournissent pas le meilleur mécanisme pour mettre en œuvre certaines abstractions. Les abstractions fonctionnelles fournissent une métaphore plus naturelle pour représenter les relations entre les entrées et les sorties. Il ne faut pas se sentir obligé d'adapter chaque élément de la logique d'un programme à une classe, en particulier quand la définition des fonctions indépendantes pour manipuler des données est plus naturelle. Les fonctions peuvent également appliquer une séparation des préoccupations.

Les langages multiparadigmes comme Python permettent aux programmeurs d'adapter les paradigmes organisationnels aux problèmes appropriés. Apprendre à identifier quand introduire une nouvelle classe, ou au contraire une nouvelle fonction, afin de simplifier ou modulariser un programme, est une compétence importante de conception en génie logiciel.


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.