Introduction
L'héritage est un outil couramment utilisé pour écrire des programmes extensibles. Cette section explore cette idée.
Héritage
L'héritage est utilisé pour spécialiser des objets existants :
class Parent:
...
class Child(Parent):
...
La nouvelle classe Child est appelée classe dérivée ou sous-classe. La classe Parent est connue sous le nom de classe de base ou super-classe. Parent est spécifié entre () après le nom de la classe, class Child(Parent):.
Étendre
Avec l'héritage, vous prenez une classe existante et :
- Ajoutez de nouvelles méthodes
- Redéfinissez certaines des méthodes existantes
- Ajoutez de nouveaux attributs aux instances
En fin de compte, vous étendez le code existant.
Exemple
Supposons que cette soit votre classe initiale :
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
def sell(self, nshares):
self.shares -= nshares
Vous pouvez modifier n'importe quelle partie de cela via l'héritage.
Ajoutez une nouvelle méthode
class MyStock(Stock):
def panic(self):
self.sell(self.shares)
Exemple d'utilisation.
>>> s = MyStock('GOOG', 100, 490.1)
>>> s.sell(25)
>>> s.shares
75
>>> s.panic()
>>> s.shares
0
>>>
Redéfinition d'une méthode existante
class MyStock(Stock):
def cost(self):
return 1.25 * self.shares * self.price
Exemple d'utilisation.
>>> s = MyStock('GOOG', 100, 490.1)
>>> s.cost()
61262.5
>>>
La nouvelle méthode remplace l'ancienne. Les autres méthodes ne sont pas affectées. C'est formidable.
Redéfinition avec appel à la méthode originale
Parfois, une classe étend une méthode existante, mais elle veut utiliser l'implémentation originale à l'intérieur de la redéfinition. Pour ce faire, utilisez super() :
class Stock:
...
def cost(self):
return self.shares * self.price
...
class MyStock(Stock):
def cost(self):
## Vérifiez l'appel à `super`
actual_cost = super().cost()
return 1.25 * actual_cost
Utilisez super() pour appeler la version précédente.
Avertissement : En Python 2, la syntaxe était plus verbeuse.
actual_cost = super(MyStock, self).cost()
__init__ et héritage
Si __init__ est redéfini, il est essentiel d'initialiser le parent.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
class MyStock(Stock):
def __init__(self, name, shares, price, factor):
## Vérifiez l'appel à `super` et `__init__`
super().__init__(name, shares, price)
self.factor = factor
def cost(self):
return self.factor * super().cost()
Vous devriez appeler la méthode __init__() sur super qui est le moyen d'appeler la version précédente comme montré précédemment.
Utilisation de l'héritage
L'héritage est parfois utilisé pour organiser des objets apparentés.
class Shape:
...
class Circle(Shape):
...
class Rectangle(Shape):
...
Pensez à une hiérarchie logique ou une taxonomie. Cependant, une utilisation plus courante (et pratique) est liée à la création de code réutilisable ou extensible. Par exemple, un framework peut définir une classe de base et vous demander de la personnaliser.
class CustomHandler(TCPHandler):
def handle_request(self):
...
## Traitement personnalisé
La classe de base contient du code à usage général. Votre classe hérite et personnalise des parties spécifiques.
Relation "est un"
L'héritage établit une relation de type.
class Shape:
...
class Circle(Shape):
...
Vérifiez l'instance d'objet.
>>> c = Circle(4.0)
>>> isinstance(c, Shape)
True
>>>
Important : Idéalement, tout code qui fonctionnait avec des instances de la classe parent fonctionnera également avec des instances de la classe enfant.
Classe de base object
Si une classe n'a pas de parent, vous voyez parfois object utilisé comme base.
class Shape(object):
...
object est le parent de tous les objets en Python.
*Note : techniquement, ce n'est pas obligatoire, mais vous le voyez souvent spécifié comme héritage de son utilisation obligatoire en Python 2. Si omis, la classe hérite implicitement de object.
Héritage multiple
Vous pouvez hériter de plusieurs classes en les spécifiant dans la définition de la classe.
class Mother:
...
class Father:
...
class Child(Mother, Father):
...
La classe Child hérite des caractéristiques des deux parents. Il y a quelques détails assez complexes. Ne le faites pas à moins de savoir ce que vous faites. Plus d'informations seront données dans la section suivante, mais nous n'allons pas utiliser l'héritage multiple plus loin dans ce cours.
Un usage majeur de l'héritage est d'écrire du code conçu pour être étendu ou personnalisé de diverses manières - en particulier dans des bibliothèques ou des frameworks. Pour illustrer, considérez la fonction print_report() dans votre programme report.py. Elle devrait ressembler à ceci :
def print_report(reportdata):
'''
Affiche un tableau bien formaté à partir d'une liste de tuples (nom, actions, prix, variation).
'''
headers = ('Nom','Actions','Prix','Variation')
print('%10s %10s %10s %10s' % headers)
print(('-'*10 +' ')*len(headers))
for row in reportdata:
print('%10s %10d %10.2f %10.2f' % row)
Lorsque vous exécutez votre programme de rapport, vous devriez obtenir une sortie comme celle-ci :
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
Nom Actions Prix Variation
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
Exercice 4.5 : Un problème d'extension
Supposons que vous vouliez modifier la fonction print_report() pour prendre en charge diverses formats de sortie différents, tels que le texte brut, le HTML, le CSV ou le XML. Pour ce faire, vous pourriez essayer d'écrire une fonction énorme qui ferait tout. Cependant, cela entraînerait probablement un bordel impraticable à maintenir. Au lieu de cela, c'est une excellente occasion d'utiliser l'héritage à la place.
Pour commencer, concentrez-vous sur les étapes impliquées dans la création d'un tableau. En haut du tableau se trouve un ensemble d'en-têtes de tableau. Après cela, apparaissent les lignes de données du tableau. Prenons ces étapes et mettons-les dans leur propre classe. Créez un fichier appelé tableformat.py et définissez la classe suivante :
## tableformat.py
class TableFormatter:
def headings(self, headers):
'''
Émet les en-têtes de tableau.
'''
raise NotImplementedError()
def row(self, rowdata):
'''
Émet une seule ligne de données de tableau.
'''
raise NotImplementedError()
Cette classe ne fait rien, mais elle sert comme une sorte de spécification de conception pour les classes supplémentaires qui seront définies bientôt. Une classe comme celle-ci est parfois appelée une "classe de base abstraite".
Modifiez la fonction print_report() de sorte qu'elle accepte un objet TableFormatter en entrée et invoque des méthodes dessus pour produire la sortie. Par exemple, comme ceci :
## report.py
...
def print_report(reportdata, formatter):
'''
Affiche un tableau bien formaté à partir d'une liste de tuples (nom, actions, prix, variation).
'''
formatter.headings(['Nom','Actions','Prix','Variation'])
for name, shares, price, change in reportdata:
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
formatter.row(rowdata)
Depuis que vous avez ajouté un argument à print_report(), vous devrez également modifier la fonction portfolio_report(). Modifiez-la de sorte qu'elle crée un TableFormatter comme ceci :
## report.py
import tableformat
...
def portfolio_report(portfoliofile, pricefile):
'''
Génère un rapport sur les actions à partir de fichiers de données sur le portefeuille et les prix.
'''
## Lire les fichiers de données
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
## Créer les données du rapport
report = make_report_data(portfolio, prices)
## L'afficher
formatter = tableformat.TableFormatter()
print_report(report, formatter)
Exécutez ce nouveau code :
>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
... plante...
Il devrait immédiatement planter avec une exception NotImplementedError. Ce n'est pas très passionnant, mais c'est exactement ce que nous attendions. Passons à la partie suivante.
Exercice 4.6 : Utiliser l'héritage pour produire différents formats de sortie
La classe TableFormatter que vous avez définie dans la partie (a) est destinée à être étendue via l'héritage. En fait, c'est tout le concept. Pour illustrer, définissez une classe TextTableFormatter comme ceci :
## tableformat.py
...
class TextTableFormatter(TableFormatter):
'''
Affiche un tableau au format texte brut
'''
def headings(self, headers):
for h in headers:
print(f'{h:>10s}', end=' ')
print()
print(('-'*10 +' ')*len(headers))
def row(self, rowdata):
for d in rowdata:
print(f'{d:>10s}', end=' ')
print()
Modifiez la fonction portfolio_report() comme ceci et essayez-la :
## report.py
...
def portfolio_report(portfoliofile, pricefile):
'''
Génère un rapport sur les actions à partir de fichiers de données sur le portefeuille et les prix.
'''
## Lire les fichiers de données
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
## Créer les données du rapport
report = make_report_data(portfolio, prices)
## L'afficher
formatter = tableformat.TextTableFormatter()
print_report(report, formatter)
Cela devrait produire la même sortie que précédemment :
>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
Nom Actions Prix Variation
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
>>>
Cependant, modifions la sortie en quelque chose d'autre. Définissez une nouvelle classe CSVTableFormatter qui produit une sortie au format CSV :
## tableformat.py
...
class CSVTableFormatter(TableFormatter):
'''
Affiche les données du portefeuille au format CSV.
'''
def headings(self, headers):
print(','.join(headers))
def row(self, rowdata):
print(','.join(rowdata))
Modifiez votre programme principal comme suit :
def portfolio_report(portfoliofile, pricefile):
'''
Génère un rapport sur les actions à partir de fichiers de données sur le portefeuille et les prix.
'''
## Lire les fichiers de données
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
## Créer les données du rapport
report = make_report_data(portfolio, prices)
## L'afficher
formatter = tableformat.CSVTableFormatter()
print_report(report, formatter)
Vous devriez maintenant voir une sortie au format CSV comme ceci :
>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
Nom,Actions,Prix,Variation
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
En utilisant une idée similaire, définissez une classe HTMLTableFormatter qui produit un tableau avec la sortie suivante :
<tr><th>Nom</th><th>Actions</th><th>Prix</th><th>Variation</th></tr>
<tr><td>AA</td><td>100</td><td>9.22</td><td>-22.98</td></tr>
<tr><td>IBM</td><td>50</td><td>106.28</td><td>15.18</td></tr>
<tr><td>CAT</td><td>150</td><td>35.46</td><td>-47.98</td></tr>
<tr><td>MSFT</td><td>200</td><td>20.89</td><td>-30.34</td></tr>
<tr><td>GE</td><td>95</td><td>13.48</td><td>-26.89</td></tr>
<tr><td>MSFT</td><td>50</td><td>20.89</td><td>-44.21</td></tr>
<tr><td>IBM</td><td>100</td><td>106.28</td><td>35.84</td></tr>
Testez votre code en modifiant le programme principal pour créer un objet HTMLTableFormatter au lieu d'un objet CSVTableFormatter.
Exercice 4.7 : Le polymorphisme en action
Une caractéristique majeure de la programmation orientée objet est que vous pouvez insérer un objet dans un programme et qu'il fonctionnera sans avoir à modifier aucun des codes existants. Par exemple, si vous écriviez un programme qui devait utiliser un objet TableFormatter, il fonctionnerait peu importe le type d'objet TableFormatter que vous lui donniez réellement. Ce comportement est parfois appelé "polymorphisme".
Un problème potentiel est de trouver comment permettre à l'utilisateur de choisir le formatteur qu'il veut. L'utilisation directe des noms de classes tels que TextTableFormatter est souvent gênante. Ainsi, vous pourriez envisager une approche simplifiée. Peut-être insérez-vous une instruction if dans le code comme ceci :
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
'''
Génère un rapport sur les actions à partir de fichiers de données sur le portefeuille et les prix.
'''
## Lire les fichiers de données
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
## Créer les données du rapport
report = make_report_data(portfolio, prices)
## L'afficher
if fmt == 'txt':
formatter = tableformat.TextTableFormatter()
elif fmt == 'csv':
formatter = tableformat.CSVTableFormatter()
elif fmt == 'html':
formatter = tableformat.HTMLTableFormatter()
else:
raise RuntimeError(f'Format inconnu {fmt}')
print_report(report, formatter)
Dans ce code, l'utilisateur spécifie un nom simplifié tel que 'txt' ou 'csv' pour choisir un format. Cependant, est-ce que mettre une grosse instruction if dans la fonction portfolio_report() comme ça est la meilleure idée? Il serait peut-être mieux de déplacer ce code vers une fonction générale ailleurs.
Dans le fichier tableformat.py, ajoutez une fonction create_formatter(name) qui permet à l'utilisateur de créer un formatteur en donnant un nom de sortie tel que 'txt', 'csv' ou 'html'. Modifiez portfolio_report() de sorte qu'elle ressemble à ceci :
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
'''
Génère un rapport sur les actions à partir de fichiers de données sur le portefeuille et les prix.
'''
## Lire les fichiers de données
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
## Créer les données du rapport
report = make_report_data(portfolio, prices)
## L'afficher
formatter = tableformat.create_formatter(fmt)
print_report(report, formatter)
Essayez d'appeler la fonction avec différents formats pour vous assurer qu'elle fonctionne.
Exercice 4.8 : Mettre tout ça ensemble
Modifiez le programme report.py de sorte que la fonction portfolio_report() prenne un argument optionnel spécifiant le format de sortie. Par exemple :
>>> report.portfolio_report('portfolio.csv', 'prices.csv', 'txt')
Nom Actions Prix Variation
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
>>>
Modifiez le programme principal de sorte qu'un format puisse être spécifié sur la ligne de commande :
$ python3 report.py portfolio.csv prices.csv csv
Nom,Actions,Prix,Variation
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
$
Discussion
Écrire du code extensible est l'une des utilisations les plus courantes de l'héritage dans les bibliothèques et les frameworks. Par exemple, un framework pourrait vous demander de définir votre propre objet qui hérite d'une classe de base fournie. Vous êtes ensuite invité à compléter diverses méthodes qui implémentent divers aspects de la fonctionnalité.
Un autre concept un peu plus approfondi est l'idée d'"être propriétaire de vos abstractions". Dans les exercices, nous avons défini notre propre classe pour formater un tableau. Vous pouvez regarder votre code et vous dire "Je devrais plutôt utiliser une bibliothèque de formatage ou quelque chose que quelqu'un a déjà créé!". Non, vous devriez utiliser à la fois votre classe et une bibliothèque. Utiliser votre propre classe favorise la faible couplage et est plus flexible. Tant que votre application utilise l'interface de programmation de votre classe, vous pouvez changer la mise en œuvre interne pour qu'elle fonctionne de la manière que vous voulez. Vous pouvez écrire du code entièrement personnalisé. Vous pouvez utiliser un package tiers. Vous remplacez un package tiers par un autre package différent lorsque vous trouvez un meilleur. Cela n'a pas d'importance - aucun de votre code d'application ne casserait tant que vous conservez l'interface. C'est une idée puissante et c'est l'une des raisons pour lesquelles vous pourriez considérer l'héritage pour quelque chose comme cela.
Cela étant dit, concevoir des programmes orientés objet peut être extrêmement difficile. Pour en savoir plus, vous devriez probablement chercher des livres sur le sujet des patrons de conception (bien que comprendre ce qui s'est passé dans cet exercice vous emmène assez loin en termes d'utilisation des objets d'une manière pratiquement utile).
Sommaire
Félicitations! Vous avez terminé le laboratoire sur l'héritage. Vous pouvez pratiquer d'autres laboratoires sur LabEx pour améliorer vos compétences.