Formation gratuite Python
- Gutenberg et votre contenu WordPress existant
- Luminaires de style pytest xUnit – Tests Python
- 10+ thèmes WordPress pour les meilleurs avocats pour les cabinets d'avocats
- Comment installer en vrac plusieurs plug-ins dans WordPress à l'aide de WP-CLI
- Épisode 180: Nouveautés de Python 3.7 et des versions ultérieures
Dans cet article, vous allez explorer héritage et composition en Python. L'héritage et la composition sont deux concepts importants de la programmation orientée objet qui modélisent la relation entre deux classes. Ils constituent les éléments de base de la conception orientée objet et aident les programmeurs à écrire du code réutilisable.
À la fin de cet article, vous saurez comment:
- Utiliser l'héritage en Python
- Hiérarchies de classes de modèles utilisant l'héritage
- Utiliser l'héritage multiple en Python et comprendre ses inconvénients
- Utiliser la composition pour créer des objets complexes
- Réutiliser le code existant en appliquant une composition
- Changer le comportement de l'application au moment de l'exécution via la composition
Que sont l'héritage et la composition?
Héritage et composition Deux concepts majeurs de la programmation orientée objet modélisent la relation entre deux classes. Ils déterminent la conception d'une application et déterminent son évolution au fur et à mesure de l'ajout de nouvelles fonctionnalités ou de la modification des exigences.
Les deux permettent la réutilisation du code, mais de manière différente.
Qu'est-ce que l'héritage?
Héritage modélise ce qu'on appelle un est un relation. Cela signifie que lorsque vous avez un Dérivé
classe qui hérite d'un Base
classe, vous avez créé une relation où Dérivé
est un version spécialisée de Base
.
L'héritage est représenté à l'aide du langage de modélisation unifié ou UML de la manière suivante:
Les classes sont représentées sous forme de boîtes avec le nom de la classe en haut. La relation d'héritage est représentée par une flèche de la classe dérivée pointant vers la classe de base. Le mot s'étend est généralement ajouté à la flèche.
Remarque: Dans une relation d'héritage:
- Les classes qui héritent des autres sont appelées classes dérivées, sous-classes ou sous-types.
- Les classes à partir desquelles d'autres classes sont dérivées sont appelées classes de base ou super classes.
- Une classe dérivée est dite dériver, hériter ou étendre une classe de base.
Disons que vous avez une classe de base Animal
et vous en dérivez pour créer un Cheval
classe. La relation d'héritage dit qu'un Cheval
est un Animal
. Cela signifie que Cheval
hérite de l'interface et de la mise en œuvre de Animal
, et Cheval
les objets peuvent être utilisés pour remplacer Animal
objets dans l'application.
Ceci est connu comme le principe de substitution de Liskov. Le principe stipule que “dans un programme informatique, si S
est un sous-type de T
, puis objets de type T
peut être remplacé par des objets de type S
sans modifier aucune des propriétés souhaitées du programme ”.
Vous verrez dans cet article pourquoi vous devriez toujours suivre le principe de substitution de Liskov lors de la création de vos hiérarchies de classes, ainsi que les problèmes que vous risquez de rencontrer si vous ne le faites pas.
Quelle est la composition?
Composition est un concept qui modélise un a un relation. Il permet de créer des types complexes en combinant des objets d'autres types. Cela signifie qu'une classe Composite
peut contenir un objet d'une autre classe Composant
. Cette relation signifie qu'un Composite
a un Composant
.
UML représente la composition comme suit:
La composition est représentée par une ligne avec un diamant dans la classe composite pointant vers la classe de composants. Le côté composite peut exprimer la cardinalité de la relation. La cardinalité indique le nombre ou la plage valide de Composite
cas les Composant
la classe contiendra.
Dans le diagramme ci-dessus, le 1
représente que le Composite
la classe contient un objet de type Composant
. La cardinalité peut être exprimée de la manière suivante:
- Un numéro indique le nombre de
Composant
cas qui sont contenus dans leComposite
. - Le symbole indique que le
Composite
classe peut contenir un nombre variable deComposant
les instances. - Une gamme 1..4 indique que le
Composite
classe peut contenir une gamme deComposant
les instances. La plage est indiquée avec le nombre minimum et maximum d'instances, ou le nombre minimum d'instances, comme dans 1..*.
Remarque: Les classes contenant des objets d'autres classes sont généralement appelées composites, les classes utilisées pour créer des types plus complexes étant appelées composants.
Par exemple, votre Cheval
la classe peut être composée par un autre objet de type Queue
. La composition vous permet d’exprimer cette relation en disant une Cheval
a un Queue
.
La composition vous permet de réutiliser du code en ajoutant des objets à d'autres objets, par opposition à l'héritage de l'interface et à la mise en œuvre d'autres classes. Tous les deux Cheval
et Chien
les classes peuvent tirer parti de la fonctionnalité de Queue
à travers la composition sans dériver une classe de l'autre.
Un aperçu de l'héritage en Python
Tout en Python est un objet. Les modules sont des objets, les définitions de classe et les fonctions sont des objets, et bien entendu, les objets créés à partir de classes sont également des objets.
L'héritage est une fonctionnalité requise de chaque langage de programmation orienté objet. Cela signifie que Python prend en charge l’héritage et, comme vous le verrez plus loin, l’un des rares langages prenant en charge l’héritage multiple.
Lorsque vous écrivez du code Python à l'aide de classes, vous utilisez l'héritage, même si vous ne savez pas que vous l'utilisez. Voyons ce que cela signifie.
L'objet super classe
Le moyen le plus simple de visualiser l'héritage en Python consiste à accéder au shell interactif Python et à écrire un peu de code. Vous commencerez par écrire la classe la plus simple possible:
>>> classe Ma classe:
... passer
...
Vous avez déclaré une classe Ma classe
cela ne fait pas grand chose, mais cela illustrera les concepts les plus fondamentaux de l'héritage. Maintenant que vous avez la classe déclarée, vous pouvez utiliser le dir ()
fonction pour lister ses membres:
>>> c = Ma classe()
>>> dir(c)
['__class__''__delattr__''__dict__''__dir__''__doc__''__eq__'['__class__''__delattr__''__dict__''__dir__''__doc__''__eq__'['__class__''__delattr__''__dict__''__dir__''__doc__''__eq__'['__class__''__delattr__''__dict__''__dir__''__doc__''__eq__'
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__']
dir ()
retourne une liste de tous les membres de l'objet spécifié. Vous n'avez déclaré aucun membre dans Ma classe
, alors d'où vient la liste? Vous pouvez trouver en utilisant l'interprète interactif:
>>> o = objet()
>>> dir(o)
['__class__''__delattr__''__dir__''__doc__''__eq__''__format__'['__class__''__delattr__''__dir__''__doc__''__eq__''__format__'['__class__''__delattr__''__dir__''__doc__''__eq__''__format__'['__class__''__delattr__''__dir__''__doc__''__eq__''__format__'
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__']
Comme vous pouvez le constater, les deux listes sont presque identiques. Il y a quelques membres supplémentaires dans Ma classe
comme __dict__
et __weakref__
, mais chaque membre de la objet
la classe est également présente dans Ma classe
.
En effet, chaque classe que vous créez en Python dérive implicitement de objet
. Vous pourriez être plus explicite et écrire classe MyClass (objet):
, mais c’est redondant et inutile.
Remarque: En Python 2, vous devez explicitement dériver de objet
pour des raisons autres que celles de cet article, mais vous pouvez en savoir plus à ce sujet dans la section Classes de styles et classiques de la documentation de Python 2.
Les exceptions sont une exception
Chaque classe que vous créez en Python sera implicitement dérivée de objet
. Les exceptions à cette règle sont les classes utilisées pour indiquer des erreurs en levant une exception.
Vous pouvez voir le problème à l'aide de l'interpréteur interactif Python:
>>> classe Mon erreur:
... passer
...
>>> élever Mon erreur()
Traceback (dernier appel le plus récent):
Fichier "" , ligne 1, dans
Erreur-type: les exceptions doivent dériver de BaseException
Vous avez créé une nouvelle classe pour indiquer un type d'erreur. Ensuite, vous avez essayé de l'utiliser pour générer une exception. Une exception est déclenchée mais la sortie indique que l'exception est de type Erreur-type
ne pas Mon erreur
et que tout les exceptions doivent dériver de BaseException
.
BaseException
est une classe de base fournie pour tous les types d'erreur. Pour créer un nouveau type d'erreur, vous devez dériver votre classe BaseException
ou l'une de ses classes dérivées. La convention en Python consiste à dériver vos types d'erreur personnalisés à partir de Exception
, qui à son tour dérive de BaseException
.
La manière correcte de définir votre type d'erreur est la suivante:
>>> classe Mon erreur(Exception):
... passer
...
>>> élever Mon erreur()
Traceback (dernier appel le plus récent):
Fichier "" , ligne 1, dans
__main __. MyError
Comme vous pouvez le voir, quand vous relancez Mon erreur
, la sortie indique correctement le type d'erreur générée.
Création de hiérarchies de classes
L’héritage est le mécanisme que vous utiliserez pour créer des hiérarchies de classes liées. Ces classes liées partageront une interface commune qui sera définie dans les classes de base. Les classes dérivées peuvent spécialiser l'interface en fournissant une implémentation particulière où s'applique.
Dans cette section, vous allez commencer à modéliser un système de ressources humaines. L'exemple montrera l'utilisation de l'héritage et comment les classes dérivées peuvent fournir une implémentation concrète de l'interface de classe de base.
Le système des ressources humaines doit traiter la masse salariale des employés de l’entreprise, mais il existe différents types d’employés en fonction du mode de calcul de leur masse salariale.
Vous commencez par implémenter un Système de paie
classe qui traite la paie:
# En hr.py
classe Système de paie:
def calcule_payroll(soi, employés):
impression('Calcul de la paie')
impression('====================')
pour employé dans employés:
impression(F'Paie pour: employee.id - Nom de l'employé')
impression(F'- Montant du chèque: employee.calculate_payroll ()')
impression('')
le Système de paie
met en œuvre un .calculate_payroll ()
méthode qui prend une collection d'employés et imprime leur identifiant
, prénom
et vérifiez le montant en utilisant le .calculate_payroll ()
méthode exposée sur chaque objet employé.
Maintenant, vous implémentez une classe de base Employé
qui gère l'interface commune à chaque type d'employé:
# En hr.py
classe Employé:
def __init__(soi, identifiant, prénom):
soi.identifiant = identifiant
soi.prénom = prénom
Employé
est la classe de base pour tous les types d'employés. Il est construit avec un identifiant
et un prénom
. Ce que vous dites, c'est que chaque Employé
doit avoir un identifiant
attribué ainsi qu'un nom.
Le système des ressources humaines exige que chaque Employé
traité doit fournir un .calculate_payroll ()
interface qui renvoie le salaire hebdomadaire de l'employé. La mise en oeuvre de cette interface diffère selon le type de Employé
.
Par exemple, les employés administratifs ont un salaire fixe. Ainsi, chaque semaine, ils reçoivent le même salaire:
# En hr.py
classe SalaireEmployé(Employé):
def __init__(soi, identifiant, prénom, hebdomadaire_salaire):
super().__init__(identifiant, prénom)
soi.hebdomadaire_salaire = hebdomadaire_salaire
def calcule_payroll(soi):
revenir soi.hebdomadaire_salaire
Vous créez une classe dérivée SalaireEmployé
qui hérite Employé
. La classe est initialisée avec le identifiant
et prénom
requis par la classe de base, et vous utilisez super()
pour initialiser les membres de la classe de base. Vous pouvez tout lire sur super()
dans Supercharge vos cours avec Python super ().
SalaireEmployé
nécessite également un hebdomadaire_salaire
Paramètre d'initialisation qui représente le montant que l'employé gagne par semaine.
La classe fournit le nécessaire .calculate_payroll ()
méthode utilisée par le système RH. L’implémentation ne fait que renvoyer le montant stocké dans hebdomadaire_salaire
.
La société emploie également des ouvriers de fabrication rémunérés à l’heure. Vous ajoutez donc un Horaire horaire
au système des ressources humaines:
# En hr.py
classe Horaire horaire(Employé):
def __init__(soi, identifiant, prénom, heures travaillées, Taux horaire):
super().__init__(identifiant, prénom)
soi.heures travaillées = heures travaillées
soi.Taux horaire = Taux horaire
def calcule_payroll(soi):
revenir soi.heures travaillées * soi.Taux horaire
le Horaire horaire
la classe est initialisée avec identifiant
et prénom
, comme la classe de base, plus le heures travaillées
et le Taux horaire
nécessaire pour calculer la masse salariale. le .calculate_payroll ()
Cette méthode est mise en œuvre en renvoyant les heures travaillées multipliées par le taux horaire.
Enfin, la société emploie des associés aux ventes rémunérés selon un salaire fixe plus une commission basée sur leurs ventes. Vous créez donc un CommissionEmployé
classe:
# En hr.py
classe CommissionEmployé(SalaireEmployé):
def __init__(soi, identifiant, prénom, hebdomadaire_salaire, commission):
super().__init__(identifiant, prénom, hebdomadaire_salaire)
soi.commission = commission
def calcule_payroll(soi):
fixé = super().calcule_payroll()
revenir fixé + soi.commission
Vous dérivez CommissionEmployé
de SalaireEmployé
parce que les deux classes ont un hebdomadaire_salaire
à envisager. En même temps, CommissionEmployé
est initialisé avec un commission
valeur basée sur les ventes de l'employé.
.calculate_payroll ()
tire parti de la mise en œuvre de la classe de base pour récupérer le fixé
salaire et ajoute la valeur de la commission.
Puisque CommissionEmployé
dérive de SalaireEmployé
, vous avez accès au hebdomadaire_salaire
propriété directement, et vous auriez pu implémenté .calculate_payroll ()
en utilisant la valeur de cette propriété.
Le problème avec l'accès direct à la propriété est que si la mise en œuvre de SalaryEmployee.calculate_payroll ()
changements, vous devrez également modifier la mise en œuvre de CommissionEmployee.calculate_payroll ()
. Il est préférable de s’appuyer sur la méthode déjà implémentée dans la classe de base et d’étendre les fonctionnalités au besoin.
Vous avez créé votre première hiérarchie de classes pour le système. Le diagramme UML des classes ressemble à ceci:
Le diagramme montre la hiérarchie d'héritage des classes. Les classes dérivées implémentent la IPayrollCalculator
l’interface requise par le Système de paie
. le PayrollSystem.calculate_payroll ()
la mise en œuvre nécessite que le employé
les objets passés contiennent un identifiant
, prénom
, et Calculate_payroll ()
la mise en oeuvre.
Les interfaces sont représentées de la même manière que les classes avec le mot interface au-dessus du nom de l'interface. Les noms d'interface sont généralement précédés d'une majuscule je
.
L'application crée ses employés et les transmet au système de paie pour traiter la paie:
# Dans programme.py
importation heure
salaire_employé = heure.SalaireEmployé(1, 'John Smith', 1500)
hourly_employee = heure.Horaire horaire(2, 'Jane Doe', 40, 15)
commission_employee = heure.CommissionEmployé(3, 'Kevin Bacon', 1000, 250)
système de paie = heure.Système de paie()
système de paie.calcule_payroll([[[[
salaire_employé,
hourly_employee,
commission_employee
])
Vous pouvez exécuter le programme en ligne de commande et voir les résultats:
$ programme python.py
Calcul de la paie
====================
Paie pour: 1 - John Smith
- montant du chèque: 1500
Paie pour: 2 - Jane Doe
- montant du chèque: 600
Paie pour: 3 - Kevin Bacon
- montant du chèque: 1250
Le programme crée trois objets employé, un pour chacune des classes dérivées. Ensuite, il crée le système de paie et transmet une liste des employés à son .calculate_payroll ()
Cette méthode calcule la masse salariale de chaque employé et imprime les résultats.
Remarquez comment Employé
classe de base ne définit pas un .calculate_payroll ()
méthode. Cela signifie que si vous deviez créer une plaine Employé
objet et le passer à la Système de paie
, alors vous aurez une erreur. Vous pouvez l'essayer dans l'interpréteur interactif Python:
>>> importation heure
>>> employé = heure.Employé(1, 'Invalide')
>>> système de paie = heure.Système de paie()
>>> système de paie.calcule_payroll([[[[employé])
Paie pour: 1 - Invalide
Traceback (dernier appel le plus récent):
Fichier "" , ligne 1, dans
Fichier "/hr.py", ligne 39, dans calcule_payroll
impression(F'- Montant du chèque: employee.calculate_payroll ()')
AttributeError: L'objet 'Employee' n'a pas d'attribut 'Calculate_payroll'
Bien que vous puissiez instancier un Employé
objet, cet objet ne peut pas être utilisé par le Système de paie
. Pourquoi? Parce que ça ne peut pas .calculate_payroll ()
pour un Employé
. Pour répondre aux exigences de Système de paie
, vous voudrez convertir le Employé
class, qui est actuellement une classe concrète, en une classe abstraite. De cette façon, aucun employé n'est jamais juste un Employé
, mais qui implémente .calculate_payroll ()
.
Classes de base abstraites en Python
le Employé
class dans l'exemple ci-dessus est ce qu'on appelle une classe de base abstraite. Les classes de base abstraites existent pour être héritées, mais jamais instanciées. Python fournit le abc
module pour définir les classes de base abstraites.
Vous pouvez utiliser des traits de soulignement principaux dans le nom de votre classe pour indiquer que les objets de cette classe ne doivent pas être créés. Les traits de soulignement constituent un moyen convivial d'empêcher l'utilisation abusive de votre code, mais ils n'empêchent pas les utilisateurs désireux de créer des instances de cette classe.
le abc
Le module de la bibliothèque standard Python fournit des fonctionnalités pour empêcher la création d'objets à partir de classes de base abstraites.
Vous pouvez modifier la mise en œuvre du Employé
classe pour s’assurer qu’il ne peut pas être instancié:
# En hr.py
de abc importation abc, méthode abstraite
classe Employé(abc):
def __init__(soi, identifiant, prénom):
soi.identifiant = identifiant
soi.prénom = prénom
@abstractmethod
def calcule_payroll(soi):
passer
Vous dérivez Employé
de abc
, ce qui en fait une classe de base abstraite. Ensuite, vous décorez le .calculate_payroll ()
méthode avec le @abstractmethod
décorateur.
Ce changement a deux effets secondaires intéressants:
- Vous indiquez aux utilisateurs du module que des objets de type
Employé
ne peut pas être créé. - Vous dites aux autres développeurs travaillant sur le
heure
module que s'ils dérivent deEmployé
, alors ils doivent passer outre le.calculate_payroll ()
méthode abstraite.
Vous pouvez voir que les objets de type Employé
ne peut pas être créé à l’aide de l’interprète interactif:
>>> importation heure
>>> employé = heure.Employé(1, 'abstrait')
Traceback (dernier appel le plus récent):
Fichier "" , ligne 1, dans
Erreur-type: Impossible d'instancier une classe abstraite Employee avec des méthodes abstraites
calcule_payroll
La sortie montre que la classe ne peut pas être instanciée car elle contient une méthode abstraite. Calculate_payroll ()
. Les classes dérivées doivent remplacer la méthode pour permettre la création d'objets de leur type.
Héritage d'implémentation vs héritage d'interface
Lorsque vous dérivez une classe d'une autre, la classe dérivée hérite des deux:
-
L'interface de classe de base: La classe dérivée hérite de toutes les méthodes, propriétés et attributs de la classe de base.
-
L'implémentation de la classe de base: La classe dérivée hérite du code qui implémente l'interface de classe.
La plupart du temps, vous souhaiterez hériter de l'implémentation d'une classe, mais vous voudrez également implémenter plusieurs interfaces afin que vos objets puissent être utilisés dans différentes situations.
Les langages de programmation modernes ont été conçus avec ce concept de base. Ils vous permettent d'hériter d'une seule classe, mais vous pouvez implémenter plusieurs interfaces.
En Python, il n’est pas nécessaire de déclarer explicitement une interface. Tout objet qui implémente l'interface souhaitée peut être utilisé à la place d'un autre objet. Ceci est connu comme frappe de canard. La frappe de canard s’explique généralement par «s’il se comporte comme un canard, c’est un canard».
Pour illustrer cela, vous allez maintenant ajouter un Employé mécontent
classe à l'exemple ci-dessus qui ne provient pas de Employé
:
# In disgruntled.py
classe Employé mécontent:
def __init__(soi, identifiant, prénom):
soi.identifiant = identifiant
soi.prénom = prénom
def calcule_payroll(soi):
revenir 1000000
le Employé mécontent
la classe ne dérive pas de Employé
, mais il expose la même interface requise par le Système de paie
. le PayrollSystem.calculate_payroll ()
nécessite une liste d'objets qui implémentent l'interface suivante:
- Un
identifiant
propriété ou attribut qui renvoie l'identifiant de l'employé - UNE
prénom
propriété ou attribut qui représente le nom de l'employé - UNE
.calculate_payroll ()
méthode qui ne prend aucun paramètre et renvoie le montant de la paie à traiter
Toutes ces exigences sont remplies par le Employé mécontent
classe, donc le Système de paie
peut encore calculer sa masse salariale.
Vous pouvez modifier le programme pour utiliser le Employé mécontent
classe:
# Dans programme.py
importation heure
importation mécontent
salaire_employé = heure.SalaireEmployé(1, 'John Smith', 1500)
hourly_employee = heure.Horaire horaire(2, 'Jane Doe', 40, 15)
commission_employee = heure.CommissionEmployé(3, 'Kevin Bacon', 1000, 250)
employé mécontent = mécontent.Employé mécontent(20000, 'Anonyme')
système de paie = heure.Système de paie()
système de paie.calcule_payroll([[[[
salaire_employé,
hourly_employee,
commission_employee,
employé mécontent
])
Le programme crée un Employé mécontent
objet et l'ajoute à la liste traitée par le Système de paie
. Vous pouvez maintenant exécuter le programme et voir sa sortie:
$ programme python.py
Calcul de la paie
====================
Paie pour: 1 - John Smith
- montant du chèque: 1500
Paie pour: 2 - Jane Doe
- montant du chèque: 600
Paie pour: 3 - Kevin Bacon
- montant du chèque: 1250
Paie pour: 20000 - Anonyme
- montant du chèque: 1000000
Comme vous pouvez le voir, le Système de paie
peut toujours traiter le nouvel objet car il répond à l'interface souhaitée.
Puisque vous n’avez pas à dériver d’une classe spécifique pour que vos objets soient réutilisables par le programme, vous vous demandez peut-être pourquoi vous devriez utiliser l’héritage au lieu de simplement mettre en œuvre l’interface souhaitée. Les règles suivantes peuvent vous aider:
-
Utilisez l'héritage pour réutiliser une implémentation: Vos classes dérivées doivent tirer parti de la plupart de leur implémentation de classe de base. Ils doivent également modéliser un est un relation. UNE
Client
la classe pourrait aussi avoir unidentifiant
et unprénom
, mais unClient
n'est pas unEmployé
, vous ne devriez donc pas utiliser l'héritage. -
Implémenter une interface à réutiliser: Lorsque vous souhaitez que votre classe soit réutilisée par une partie spécifique de votre application, vous implémentez l'interface requise dans votre classe, mais vous n'avez pas besoin de fournir une classe de base, ni d'hériter d'une autre classe.
Vous pouvez maintenant nettoyer l'exemple ci-dessus pour passer au sujet suivant. Vous pouvez supprimer le mécontent.py
déposer puis modifier le heure
module à son état d'origine:
# En hr.py
classe Système de paie:
def calcule_payroll(soi, employés):
impression('Calcul de la paie')
impression('====================')
pour employé dans employés:
impression(F'Paie pour: employee.id - Nom de l'employé')
impression(F'- Montant du chèque: employee.calculate_payroll ()')
impression('')
classe Employé:
def __init__(soi, identifiant, prénom):
soi.identifiant = identifiant
soi.prénom = prénom
classe SalaireEmployé(Employé):
def __init__(soi, identifiant, prénom, hebdomadaire_salaire):
super().__init__(identifiant, prénom)
soi.hebdomadaire_salaire = hebdomadaire_salaire
def calcule_payroll(soi):
revenir soi.hebdomadaire_salaire
classe Horaire horaire(Employé):
def __init__(soi, identifiant, prénom, heures travaillées, Taux horaire):
super().__init__(identifiant, prénom)
soi.heures travaillées = heures travaillées
soi.Taux horaire = Taux horaire
def calcule_payroll(soi):
revenir soi.heures travaillées * soi.Taux horaire
classe CommissionEmployé(SalaireEmployé):
def __init__(soi, identifiant, prénom, hebdomadaire_salaire, commission):
super().__init__(identifiant, prénom, hebdomadaire_salaire)
soi.commission = commission
def calcule_payroll(soi):
fixé = super().calcule_payroll()
revenir fixé + soi.commission
Vous avez supprimé l'importation du abc
module depuis le Employé
Il n’est pas nécessaire que la classe soit abstraite. Vous avez également supprimé le résumé Calculate_payroll ()
méthode car elle ne fournit aucune implémentation.
Fondamentalement, vous héritez de la mise en œuvre de la identifiant
et prénom
attributs de la Employé
classe dans vos classes dérivées. Puisque .calculate_payroll ()
est juste une interface à la PayrollSystem.calculate_payroll ()
méthode, vous n'avez pas besoin de l'implémenter dans le Employé
classe de base.
Remarquez comment CommissionEmployé
la classe dérive de SalaireEmployé
. Cela signifie que CommissionEmployé
hérite de la mise en œuvre et de l'interface de SalaireEmployé
. Vous pouvez voir comment le CommissionEmployee.calculate_payroll ()
Cette méthode exploite l’implémentation de la classe de base car elle repose sur le résultat de super (). Calculate_payroll ()
mettre en œuvre sa propre version.
Le problème de l'explosion de classe
Si vous ne faites pas attention, l'héritage peut vous conduire à une énorme structure hiérarchique de classes difficile à comprendre et à maintenir. Ceci est connu comme le problème d'explosion de classe.
Vous avez commencé à construire une hiérarchie de classe de Employé
types utilisés par le Système de paie
calculer la masse salariale. Maintenant, vous devez ajouter des fonctionnalités à ces classes afin qu’elles puissent être utilisées avec le nouveau Système de productivité
.
le Système de productivité
suit la productivité en fonction des rôles des employés. Il y a différents rôles d'employé:
- Gestionnaires: Ils se promènent en criant après que des gens leur disent quoi faire. Ils sont salariés et gagnent plus d'argent.
- Secrétaires: Ils font tout le travail de papier pour les gestionnaires et veillent à ce que tout soit facturé et payé à temps. Ils sont aussi des employés mais gagnent moins d’argent.
- Employés des ventes: Ils passent beaucoup d'appels téléphoniques pour vendre leurs produits. Ils ont un salaire, mais ils reçoivent aussi des commissions sur les ventes.
- Travailleurs d'usine: Ils fabriquent les produits pour l'entreprise. Ils sont payés à l'heure.
Avec ces exigences, vous commencez à voir que Employé
et ses classes dérivées pourraient appartenir à un endroit autre que le heure
module car maintenant ils sont aussi utilisés par les Système de productivité
.
Vous créez un employés
module et déplacez les classes ici:
# Dans employés.py
classe Employé:
def __init__(soi, identifiant, prénom):
soi.identifiant = identifiant
soi.prénom = prénom
classe SalaireEmployé(Employé):
def __init__(soi, identifiant, prénom, hebdomadaire_salaire):
super().__init__(identifiant, prénom)
soi.hebdomadaire_salaire = hebdomadaire_salaire
def calcule_payroll(soi):
revenir soi.hebdomadaire_salaire
classe Salaire horaire(Employé):
def __init__(soi, identifiant, prénom, heures travaillées, Taux horaire):
super().__init__(identifiant, prénom)
soi.heures travaillées = heures travaillées
soi.Taux horaire = Taux horaire
def calcule_payroll(soi):
revenir soi.heures travaillées * soi.Taux horaire
classe CommissionEmployé(SalaireEmployé):
def __init__(soi, identifiant, prénom, hebdomadaire_salaire, commission):
super().__init__(identifiant, prénom, hebdomadaire_salaire)
soi.commission = commission
def calcule_payroll(soi):
fixé = super().calcule_payroll()
revenir fixé + soi.commission
L'implémentation reste la même, mais vous déplacez les classes vers le employé
module. Maintenant, vous modifiez votre programme pour supporter le changement:
# Dans programme.py
importation heure
importation employés
salaire_employé = employés.SalaireEmployé(1, 'John Smith', 1500)
hourly_employee = employés.Horaire horaire(2, 'Jane Doe', 40, 15)
commission_employee = employés.CommissionEmployé(3, 'Kevin Bacon', 1000, 250)
système de paie = heure.Système de paie()
système de paie.calcule_payroll([[[[
salaire_employé,
hourly_employee,
commission_employee
])
Vous exécutez le programme et vérifiez qu'il fonctionne toujours:
$ programme python.py
Calcul de la paie
====================
Paie pour: 1 - John Smith
- montant du chèque: 1500
Paie pour: 2 - Jane Doe
- montant du chèque: 600
Paie pour: 3 - Kevin Bacon
- montant du chèque: 1250
Lorsque tout est en place, vous commencez à ajouter les nouvelles classes:
# Dans employés.py
classe Directeur(SalaireEmployé):
def travail(soi, heures):
impression(F'self.name hurle et crie pour heures heures.')
classe secrétaire(SalaireEmployé):
def travail(soi, heures):
impression(F'self.name dépense heures heures de travail de bureau.)
classe Vendeur(CommissionEmployé):
def travail(soi, heures):
impression(F'self.name dépense heures heures au téléphone.)
classe Ouvrier(Horaire horaire):
def travail(soi, heures):
impression(F'self.name fabrique des gadgets pour heures heures.')
Tout d'abord, vous ajoutez un Directeur
classe qui dérive de SalaireEmployé
. La classe expose une méthode travail()
qui sera utilisé par le système de productivité. La méthode prend la heures
l'employé a travaillé.
Ensuite, vous ajoutez secrétaire
, Vendeur
, et Ouvrier
puis mettre en œuvre le travail()
interface, afin qu'ils puissent être utilisés par le système de productivité.
Maintenant, vous pouvez ajouter le ProductivitéSytem
classe:
# Dans la productivité.py
classe Système de productivité:
def Piste(soi, employés, heures):
impression("Suivi de la productivité des employés")
impression('==============================')
pour employé dans employés:
employé.travail(heures)
impression('')
La classe suit les employés dans le Piste()
méthode qui prend une liste des employés et le nombre d'heures à suivre. Vous pouvez maintenant ajouter le système de productivité à votre programme:
# Dans programme.py
importation heure
importation employés
importation productivité
directeur = employés.Directeur(1, 'Mary Poppins', 3000)
secrétaire = employés.secrétaire(2, 'John Smith', 1500)
sales_guy = employés.Vendeur(3, 'Kevin Bacon', 1000, 250)
ouvrier = employés.Ouvrier(2, 'Jane Doe', 40, 15)
employés = [[[[
directeur,
secrétaire,
sales_guy,
ouvrier,
]
système de productivité = productivité.Système de productivité()
système de productivité.Piste(employés, 40)
système de paie = heure.Système de paie()
système de paie.calcule_payroll(employés)
Le programme crée une liste d'employés de différents types. La liste des employés est envoyée au système de productivité pour suivre leur travail pendant 40 heures. Ensuite, la même liste d'employés est envoyée au système de paie pour calculer leur masse salariale.
Vous pouvez exécuter le programme pour voir la sortie:
$ programme python.py
Suivi de la productivité des employés
==============================
Mary Poppins hurle et crie pendant 40 heures.
John Smith consacre 40 heures à la paperasserie.
Kevin Bacon passe 40 heures au téléphone.
Jane Doe fabrique des gadgets pendant 40 heures.
Calcul de la paie
====================
Paie pour: 1 - Mary Poppins
- montant du chèque: 3000
Paie pour: 2 - John Smith
- montant du chèque: 1500
Paie pour: 3 - Kevin Bacon
- montant du chèque: 1250
Paie pour: 4 - Jane Doe
- montant du chèque: 600
Le programme montre aux employés travaillant pendant 40 heures via le système de productivité. Ensuite, il calcule et affiche la masse salariale de chacun des employés.
Le programme fonctionne comme prévu, mais vous avez dû ajouter quatre nouvelles classes pour prendre en charge les modifications. Avec l’apparition de nouvelles exigences, votre hiérarchie de classe va inévitablement s’amplifier, ce qui créera un problème d’explosion de classe: vos hiérarchies deviendront si grandes qu’elles seront difficiles à comprendre et à maintenir.
Le diagramme suivant montre la nouvelle hiérarchie de classes:
Le diagramme montre l'évolution de la hiérarchie des classes. Des exigences supplémentaires peuvent avoir un effet exponentiel sur le nombre de classes avec cette conception.
Héritage de plusieurs classes
Python est l’un des rares langages de programmation modernes à prendre en charge l’héritage multiple. L'héritage multiple est la capacité de dériver une classe de plusieurs classes de base en même temps.
L'héritage multiple a une mauvaise réputation dans la mesure où la plupart des langages de programmation modernes ne le prennent pas en charge. Au lieu de cela, les langages de programmation modernes supportent le concept d'interfaces. Dans ces langages, vous héritez d'une classe de base unique, puis implémentez plusieurs interfaces afin que votre classe puisse être réutilisée dans différentes situations.
Cette approche impose des contraintes à vos conceptions. Vous ne pouvez hériter de l'implémentation d'une classe qu'en en dérivant directement. Vous pouvez implémenter plusieurs interfaces, mais vous ne pouvez pas hériter de l'implémentation de plusieurs classes.
Cette contrainte est utile pour la conception de logiciels car elle vous oblige à concevoir vos classes avec moins de dépendances les unes par rapport aux autres. Vous verrez plus loin dans cet article que vous pouvez exploiter plusieurs implémentations grâce à la composition, ce qui rend le logiciel plus flexible. Toutefois, cette section traite de l’héritage multiple, examinons donc son fonctionnement.
Il se trouve que des secrétaires temporaires sont parfois embauchés lorsqu'il y a trop de paperasse à faire. le Secrétaire temporaire
la classe joue le rôle d'un secrétaire
dans le cadre de la Système de productivité
, mais aux fins de la paie, c’est un Salaire horaire
.
Vous regardez la conception de votre classe. Il a un peu grandi, mais vous pouvez toujours comprendre comment cela fonctionne. Il semble que vous ayez deux options:
-
Tirer de
secrétaire
: Vous pouvez dériver desecrétaire
hériter du.travail()
méthode pour le rôle, puis remplacez la.calculate_payroll ()
méthode pour l'implémenter en tant queSalaire horaire
. -
Tirer de
Salaire horaire
: Vous pouvez dériver deSalaire horaire
hériter du.calculate_payroll ()
méthode, puis remplacez la.travail()
méthode pour l'implémenter en tant quesecrétaire
.
Ensuite, vous vous souvenez que Python prend en charge l'héritage multiple, vous décidez donc de dériver des deux secrétaire
et Salaire horaire
:
# Dans employés.py
classe Secrétaire temporaire(secrétaire, Salaire horaire):
passer
Python vous permet d’hériter de deux classes différentes en les spécifiant entre parenthèses dans la déclaration de classe.
Maintenant, vous modifiez votre programme pour ajouter le nouvel employé de secrétaire temporaire:
importation heure
importation employés
importation productivité
directeur = employés.Directeur(1, 'Mary Poppins', 3000)
secrétaire = employés.secrétaire(2, 'John Smith', 1500)
sales_guy = employés.Vendeur(3, 'Kevin Bacon', 1000, 250)
ouvrier = employés.Ouvrier(4, 'Jane Doe', 40, 15)
secrétaire temporaire = employés.Secrétaire temporaire(5, 'Robin Williams', 40, 9)
employés de l'entreprise = [[[[
directeur,
secrétaire,
sales_guy,
ouvrier,
secrétaire temporaire,
]
système de productivité = productivité.Système de productivité()
système de productivité.Piste(employés de l'entreprise, 40)
système de paie = heure.Système de paie()
système de paie.calcule_payroll(employés de l'entreprise)
Vous exécutez le programme pour le tester:
$ programme python.py
Traceback (dernier appel le plus récent):
Fichier ". Program.py", ligne 9, dans
interim_secretary = employee.TemporarySecretary (5, 'Robin Williams', 40, 9)
TypeError: __init __ () prend 4 arguments de position mais 5 ont été donnés
Vous obtenez un Erreur-type
exception disant que 4
arguments de position où prévu, mais 5
ont reçu.
C'est parce que tu as dérivé Secrétaire temporaire
d'abord de secrétaire
puis de Salaire horaire
, donc l'interprète essaie d'utiliser Secrétaire .__ init __ ()
initialiser l'objet.
Ok, inversons la tendance:
classe Secrétaire temporaire(HourlyEmployee, Secretary):
passer
Now, run the program again and see what happens:
$ python program.py
Traceback (most recent call last):
File ".program.py", line 9, in
temporary_secretary = employee.TemporarySecretary(5, 'Robin Williams', 40, 9)
File "employee.py", line 16, in __init__
super().__init__(id, name)
TypeError: __init__() missing 1 required positional argument: 'weekly_salary'
Now it seems you are missing a weekly_salary
parameter, which is necessary to initialize Secretary
, but that parameter doesn’t make sense in the context of a TemporarySecretary
because it’s an HourlyEmployee
.
Maybe implementing TemporarySecretary.__init__()
will help:
# In employees.py
classe TemporarySecretary(HourlyEmployee, Secretary):
def __init__(soi, identifiant, prénom, hours_worked, hour_rate):
super().__init__(identifiant, prénom, hours_worked, hour_rate)
Try it:
$ python program.py
Traceback (most recent call last):
File ".program.py", line 9, in
temporary_secretary = employee.TemporarySecretary(5, 'Robin Williams', 40, 9)
File "employee.py", line 54, in __init__
super().__init__(id, name, hours_worked, hour_rate)
File "employee.py", line 16, in __init__
super().__init__(id, name)
TypeError: __init__() missing 1 required positional argument: 'weekly_salary'
That didn’t work either. Okay, it’s time for you to dive into Python’s method resolution order (MRO) to see what’s going on.
When a method or attribute of a class is accessed, Python uses the class MRO to find it. The MRO is also used by super()
to determine which method or attribute to invoke. You can learn more about super()
in Supercharge Your Classes With Python super().
You can evaluate the TemporarySecretary
class MRO using the interactive interpreter:
>>> de employés importation TemporarySecretary
>>> TemporarySecretary.__mro__
(,
,
,
,
,
)
The MRO shows the order in which Python is going to look for a matching attribute or method. In the example, this is what happens when we create the TemporarySecretary
object:
-
le
TemporarySecretary.__init__(self, id, name, hours_worked, hour_rate)
method is called. -
le
super().__init__(id, name, hours_worked, hour_rate)
call matchesHourlyEmployee.__init__(self, id, name, hour_worked, hour_rate)
. -
HourlyEmployee
appelssuper().__init__(id, name)
, which the MRO is going to match toSecretary.__init__()
, which is inherited fromSalaryEmployee.__init__(self, id, name, weekly_salary)
.
Because the parameters don’t match, a TypeError
exception is raised.
You can bypass the MRO by reversing the inheritance order and directly calling HourlyEmployee.__init__()
as follows:
classe TemporarySecretary(Secretary, HourlyEmployee):
def __init__(soi, identifiant, prénom, hours_worked, hour_rate):
HourlyEmployee.__init__(soi, identifiant, prénom, hours_worked, hour_rate)
That solves the problem of creating the object, but you will run into a similar problem when trying to calculate payroll. You can run the program to see the problem:
$ python program.py
Tracking Employee Productivity
==============================
Mary Poppins screams and yells for 40 hours.
John Smith expends 40 hours doing office paperwork.
Kevin Bacon expends 40 hours on the phone.
Jane Doe manufactures gadgets for 40 hours.
Robin Williams expends 40 hours doing office paperwork.
Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000
Payroll for: 2 - John Smith
- Check amount: 1500
Payroll for: 3 - Kevin Bacon
- Check amount: 1250
Payroll for: 4 - Jane Doe
- Check amount: 600
Payroll for: 5 - Robin Williams
Traceback (most recent call last):
File ".program.py", line 20, in
payroll_system.calculate_payroll(employees)
File "hr.py", line 7, in calculate_payroll
print(f'- Check amount: employee.calculate_payroll()')
File "employee.py", line 12, in calculate_payroll
return self.weekly_salary
AttributeError: 'TemporarySecretary' object has no attribute 'weekly_salary'
The problem now is that because you reversed the inheritance order, the MRO is finding the .calculate_payroll()
méthode de SalariedEmployee
before the one in HourlyEmployee
. You need to override .calculate_payroll()
dans TemporarySecretary
and invoke the right implementation from it:
classe TemporarySecretary(Secretary, HourlyEmployee):
def __init__(soi, identifiant, prénom, hours_worked, hour_rate):
HourlyEmployee.__init__(soi, identifiant, prénom, hours_worked, hour_rate)
def calculate_payroll(soi):
revenir HourlyEmployee.calculate_payroll(soi)
le calculate_payroll()
method directly invokes HourlyEmployee.calculate_payroll()
to ensure that you get the correct result. You can run the program again to see it working:
$ python program.py
Tracking Employee Productivity
==============================
Mary Poppins screams and yells for 40 hours.
John Smith expends 40 hours doing office paperwork.
Kevin Bacon expends 40 hours on the phone.
Jane Doe manufactures gadgets for 40 hours.
Robin Williams expends 40 hours doing office paperwork.
Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000
Payroll for: 2 - John Smith
- Check amount: 1500
Payroll for: 3 - Kevin Bacon
- Check amount: 1250
Payroll for: 4 - Jane Doe
- Check amount: 600
Payroll for: 5 - Robin Williams
- Check amount: 360
The program now works as expected because you’re forcing the method resolution order by explicitly telling the interpreter which method we want to use.
As you can see, multiple inheritance can be confusing, especially when you run into the diamond problem.
The following diagram shows the diamond problem in your class hierarchy:
The diagram shows the diamond problem with the current class design. TemporarySecretary
uses multiple inheritance to derive from two classes that ultimately also derive from Employee
. This causes two paths to reach the Employee
base class, which is something you want to avoid in your designs.
The diamond problem appears when you’re using multiple inheritance and deriving from two classes that have a common base class. This can cause the wrong version of a method to be called.
As you’ve seen, Python provides a way to force the right method to be invoked, and analyzing the MRO can help you understand the problem.
Still, when you run into the diamond problem, it’s better to re-think the design. You will now make some changes to leverage multiple inheritance, avoiding the diamond problem.
le Employee
derived classes are used by two different systems:
-
The productivity system that tracks employee productivity.
-
The payroll system that calculates the employee payroll.
This means that everything related to productivity should be together in one module and everything related to payroll should be together in another. You can start making changes to the productivity module:
# In productivity.py
classe ProductivitySystem:
def Piste(soi, employés, heures):
impression('Tracking Employee Productivity')
impression('==============================')
pour employé dans employés:
résultat = employé.travail(heures)
impression(F'employee.name: result')
impression('')
classe ManagerRole:
def travail(soi, heures):
revenir F'screams and yells for hours hours.'
classe SecretaryRole:
def travail(soi, heures):
revenir F'expends hours hours doing office paperwork.'
classe SalesRole:
def travail(soi, heures):
revenir F'expends hours hours on the phone.'
classe FactoryRole:
def travail(soi, heures):
revenir F'manufactures gadgets for hours hours.'
le productivité
module implements the ProductivitySystem
class, as well as the related roles it supports. The classes implement the work()
interface required by the system, but they don’t derived from Employee
.
You can do the same with the heure
module:
# In hr.py
classe PayrollSystem:
def calculate_payroll(soi, employés):
impression('Calculating Payroll')
impression('===================')
pour employé dans employés:
impression(F'Payroll for: employee.id - employee.name')
impression(F'- Check amount: employee.calculate_payroll()')
impression('')
classe SalaryPolicy:
def __init__(soi, weekly_salary):
soi.weekly_salary = weekly_salary
def calculate_payroll(soi):
revenir soi.weekly_salary
classe HourlyPolicy:
def __init__(soi, hours_worked, hour_rate):
soi.hours_worked = hours_worked
soi.hour_rate = hour_rate
def calculate_payroll(soi):
revenir soi.hours_worked * soi.hour_rate
classe CommissionPolicy(SalaryPolicy):
def __init__(soi, weekly_salary, commission):
super().__init__(weekly_salary)
soi.commission = commission
def calculate_payroll(soi):
fixé = super().calculate_payroll()
revenir fixé + soi.commission
le heure
module implements the PayrollSystem
, which calculates payroll for the employees. It also implements the policy classes for payroll. As you can see, the policy classes don’t derive from Employee
anymore.
You can now add the necessary classes to the employé
module:
# In employees.py
de heure importation (
SalaryPolicy,
CommissionPolicy,
HourlyPolicy
)
de productivité importation (
ManagerRole,
SecretaryRole,
SalesRole,
FactoryRole
)
classe Employee:
def __init__(soi, identifiant, prénom):
soi.identifiant = identifiant
soi.prénom = prénom
classe Directeur(Employee, ManagerRole, SalaryPolicy):
def __init__(soi, identifiant, prénom, weekly_salary):
SalaryPolicy.__init__(soi, weekly_salary)
super().__init__(identifiant, prénom)
classe Secretary(Employee, SecretaryRole, SalaryPolicy):
def __init__(soi, identifiant, prénom, weekly_salary):
SalaryPolicy.__init__(soi, weekly_salary)
super().__init__(identifiant, prénom)
classe SalesPerson(Employee, SalesRole, CommissionPolicy):
def __init__(soi, identifiant, prénom, weekly_salary, commission):
CommissionPolicy.__init__(soi, weekly_salary, commission)
super().__init__(identifiant, prénom)
classe FactoryWorker(Employee, FactoryRole, HourlyPolicy):
def __init__(soi, identifiant, prénom, hours_worked, hour_rate):
HourlyPolicy.__init__(soi, hours_worked, hour_rate)
super().__init__(identifiant, prénom)
classe TemporarySecretary(Employee, SecretaryRole, HourlyPolicy):
def __init__(soi, identifiant, prénom, hours_worked, hour_rate):
HourlyPolicy.__init__(soi, hours_worked, hour_rate)
super().__init__(identifiant, prénom)
le employés
module imports policies and roles from the other modules and implements the different Employee
types. You are still using multiple inheritance to inherit the implementation of the salary policy classes and the productivity roles, but the implementation of each class only needs to deal with initialization.
Notice that you still need to explicitly initialize the salary policies in the constructors. You probably saw that the initializations of Directeur
et Secretary
are identical. Also, the initializations of FactoryWorker
et TemporarySecretary
are the same.
You will not want to have this kind of code duplication in more complex designs, so you have to be careful when designing class hierarchies.
Here’s the UML diagram for the new design:
The diagram shows the relationships to define the Secretary
et TemporarySecretary
using multiple inheritance, but avoiding the diamond problem.
You can run the program and see how it works:
$ python program.py
Tracking Employee Productivity
==============================
Mary Poppins: screams and yells for 40 hours.
John Smith: expends 40 hours doing office paperwork.
Kevin Bacon: expends 40 hours on the phone.
Jane Doe: manufactures gadgets for 40 hours.
Robin Williams: expends 40 hours doing office paperwork.
Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000
Payroll for: 2 - John Smith
- Check amount: 1500
Payroll for: 3 - Kevin Bacon
- Check amount: 1250
Payroll for: 4 - Jane Doe
- Check amount: 600
Payroll for: 5 - Robin Williams
- Check amount: 360
You’ve seen how inheritance and multiple inheritance work in Python. You can now explore the topic of composition.
Composition in Python
Composition is an object oriented design concept that models a a un relationship. In composition, a class known as composite contains an object of another class known to as composant. In other words, a composite class a un component of another class.
Composition allows composite classes to reuse the implementation of the components it contains. The composite class doesn’t inherit the component class interface, but it can leverage its implementation.
The composition relation between two classes is considered loosely coupled. That means that changes to the component class rarely affect the composite class, and changes to the composite class never affect the component class.
This provides better adaptability to change and allows applications to introduce new requirements without affecting existing code.
When looking at two competing software designs, one based on inheritance and another based on composition, the composition solution usually is the most flexible. You can now look at how composition works.
You’ve already used composition in our examples. If you look at the Employee
class, you’ll see that it contains two attributes:
identifiant
to identify an employee.prénom
to contain the name of the employee.
These two attributes are objects that the Employee
class has. Therefore, you can say that an Employee
a un identifiant
et a un prénom.
Another attribute for an Employee
might be an Adresse
:
# In contacts.py
classe Adresse:
def __init__(soi, rue, ville, Etat, zipcode, street2=''):
soi.rue = rue
soi.street2 = street2
soi.ville = ville
soi.Etat = Etat
soi.zipcode = zipcode
def __str__(soi):
lignes = [[[[soi.rue]
si soi.street2:
lignes.ajouter(soi.street2)
lignes.ajouter(F'self.city, self.state self.zipcode')
revenir ' n'.joindre(lignes)
You implemented a basic address class that contains the usual components for an address. You made the street2
attribute optional because not all addresses will have that component.
You implemented __str__()
to provide a pretty representation of an Adresse
. You can see this implementation in the interactive interpreter:
>>> de contacts importation Adresse
>>> adresse = Adresse('55 Main St.', 'Concord', 'NH', '03301')
>>> impression(adresse)
55 Main St.
Concord, NH 03301
Lorsque vous print()
le adresse
variable, the special method __str__()
is invoked. Since you overloaded the method to return a string formatted as an address, you get a nice, readable representation. Operator and Function Overloading in Custom Python Classes gives a good overview of the special methods available in classes that can be implemented to customize the behavior of your objects.
You can now add the Adresse
au Employee
class through composition:
# In employees.py
classe Employee:
def __init__(soi, identifiant, prénom):
soi.identifiant = identifiant
soi.prénom = prénom
soi.adresse = None
You initialize the adresse
attribuer à None
for now to make it optional, but by doing that, you can now assign an Adresse
à un Employee
. Also notice that there is no reference in the employé
module to the contacts
module.
Composition is a loosely coupled relationship that often doesn’t require the composite class to have knowledge of the component.
The UML diagram representing the relationship between Employee
et Adresse
looks like this:
The diagram shows the basic composition relationship between Employee
et Adresse
.
You can now modify the PayrollSystem
class to leverage the adresse
attribute in Employee
:
# In hr.py
classe PayrollSystem:
def calculate_payroll(soi, employés):
impression('Calculating Payroll')
impression('===================')
pour employé dans employés:
impression(F'Payroll for: employee.id - employee.name')
impression(F'- Check amount: employee.calculate_payroll()')
si employé.adresse:
impression('- Sent to:')
impression(employé.adresse)
impression('')
You check to see if the employé
object has an address, and if it does, you print it. You can now modify the program to assign some addresses to the employees:
# In program.py
importation heure
importation employés
importation productivité
importation contacts
directeur = employés.Directeur(1, 'Mary Poppins', 3000)
directeur.adresse = contacts.Adresse(
'121 Admin Rd',
'Concord',
'NH',
'03301'
)
secrétaire = employés.Secretary(2, 'John Smith', 1500)
secrétaire.adresse = contacts.Adresse(
'67 Paperwork Ave.',
'Manchester',
'NH',
'03101'
)
sales_guy = employés.SalesPerson(3, 'Kevin Bacon', 1000, 250)
factory_worker = employés.FactoryWorker(4, 'Jane Doe', 40, 15)
temporary_secretary = employés.TemporarySecretary(5, 'Robin Williams', 40, 9)
employés = [[[[
directeur,
secrétaire,
sales_guy,
factory_worker,
temporary_secretary,
]
productivity_system = productivité.ProductivitySystem()
productivity_system.Piste(employés, 40)
payroll_system = heure.PayrollSystem()
payroll_system.calculate_payroll(employés)
You added a couple of addresses to the directeur
et secrétaire
objects. When you run the program, you will see the addresses printed:
$ python program.py
Tracking Employee Productivity
==============================
Mary Poppins: screams and yells for hours hours.
John Smith: expends hours hours doing office paperwork.
Kevin Bacon: expends hours hours on the phone.
Jane Doe: manufactures gadgets for hours hours.
Robin Williams: expends hours hours doing office paperwork.
Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000
- Sent to:
121 Admin Rd
Concord, NH 03301
Payroll for: 2 - John Smith
- Check amount: 1500
- Sent to:
67 Paperwork Ave.
Manchester, NH 03101
Payroll for: 3 - Kevin Bacon
- Check amount: 1250
Payroll for: 4 - Jane Doe
- Check amount: 600
Payroll for: 5 - Robin Williams
- Check amount: 360
Notice how the payroll output for the directeur
et secrétaire
objects show the addresses where the checks were sent.
le Employee
class leverages the implementation of the Adresse
class without any knowledge of what an Adresse
object is or how it’s represented. This type of design is so flexible that you can change the Adresse
class without any impact to the Employee
class.
Flexible Designs With Composition
Composition is more flexible than inheritance because it models a loosely coupled relationship. Changes to a component class have minimal or no effects on the composite class. Designs based on composition are more suitable to change.
You change behavior by providing new components that implement those behaviors instead of adding new classes to your hierarchy.
Take a look at the multiple inheritance example above. Imagine how new payroll policies will affect the design. Try to picture what the class hierarchy will look like if new roles are needed. As you saw before, relying too heavily on inheritance can lead to class explosion.
The biggest problem is not so much the number of classes in your design, but how tightly coupled the relationships between those classes are. Tightly coupled classes affect each other when changes are introduced.
In this section, you are going to use composition to implement a better design that still fits the requirements of the PayrollSystem
et le ProductivitySystem
.
You can start by implementing the functionality of the ProductivitySystem
:
# In productivity.py
classe ProductivitySystem:
def __init__(soi):
soi._roles =
'manager': ManagerRole,
'secretary': SecretaryRole,
'sales': SalesRole,
'factory': FactoryRole,
def get_role(soi, role_id):
role_type = soi._roles.obtenir(role_id)
si ne pas role_type:
élever ValueError('role_id')
revenir role_type()
def Piste(soi, employés, heures):
impression('Tracking Employee Productivity')
impression('==============================')
pour employé dans employés:
employé.travail(heures)
impression('')
le ProductivitySystem
class defines some roles using a string identifier mapped to a role class that implements the role. It exposes a .get_role()
method that, given a role identifier, returns the role type object. If the role is not found, then a ValueError
exception is raised.
It also exposes the previous functionality in the .track()
method, where given a list of employees it tracks the productivity of those employees.
You can now implement the different role classes:
# In productivity.py
classe ManagerRole:
def perform_duties(soi, heures):
revenir F'screams and yells for hours hours.'
classe SecretaryRole:
def perform_duties(soi, heures):
revenir F'does paperwork for hours hours.'
classe SalesRole:
def perform_duties(soi, heures):
revenir F'expends hours hours on the phone.'
classe FactoryRole:
def perform_duties(soi, heures):
revenir F'manufactures gadgets for hours hours.'
Each of the roles you implemented expose a .perform_duties()
that takes the number of heures
worked. The methods return a string representing the duties.
The role classes are independent of each other, but they expose the same interface, so they are interchangeable. You’ll see later how they are used in the application.
Now, you can implement the PayrollSystem
for the application:
# In hr.py
classe PayrollSystem:
def __init__(soi):
soi._employee_policies =
1: SalaryPolicy(3000),
2: SalaryPolicy(1500),
3: CommissionPolicy(1000, 100),
4: HourlyPolicy(15),
5: HourlyPolicy(9)
def get_policy(soi, employee_id):
politique = soi._employee_policies.obtenir(employee_id)
si ne pas politique:
revenir ValueError(employee_id)
revenir politique
def calculate_payroll(soi, employés):
impression('Calculating Payroll')
impression('===================')
pour employé dans employés:
impression(F'Payroll for: employee.id - employee.name')
impression(F'- Check amount: employee.calculate_payroll()')
si employé.adresse:
impression('- Sent to:')
impression(employé.adresse)
impression('')
le PayrollSystem
keeps an internal database of payroll policies for each employee. It exposes a .get_policy()
that, given an employee identifiant
, returns its payroll policy. If a specified identifiant
doesn’t exist in the system, then the method raises a ValueError
exception.
The implementation of .calculate_payroll()
works the same as before. It takes a list of employees, calculates the payroll, and prints the results.
You can now implement the payroll policy classes:
# In hr.py
classe PayrollPolicy:
def __init__(soi):
soi.hours_worked = 0
def track_work(soi, heures):
soi.hours_worked += heures
classe SalaryPolicy(PayrollPolicy):
def __init__(soi, weekly_salary):
super().__init__()
soi.weekly_salary = weekly_salary
def calculate_payroll(soi):
revenir soi.weekly_salary
classe HourlyPolicy(PayrollPolicy):
def __init__(soi, hour_rate):
super().__init__()
soi.hour_rate = hour_rate
def calculate_payroll(soi):
revenir soi.hours_worked * soi.hour_rate
classe CommissionPolicy(SalaryPolicy):
def __init__(soi, weekly_salary, commission_per_sale):
super().__init__(weekly_salary)
soi.commission_per_sale = commission_per_sale
@property
def commission(soi):
Ventes = soi.hours_worked / 5
revenir Ventes * soi.commission_per_sale
def calculate_payroll(soi):
fixé = super().calculate_payroll()
revenir fixé + soi.commission
You first implement a PayrollPolicy
class that serves as a base class for all the payroll policies. This class tracks the hours_worked
, which is common to all payroll policies.
The other policy classes derive from PayrollPolicy
. We use inheritance here because we want to leverage the implementation of PayrollPolicy
. Également, SalaryPolicy
, HourlyPolicy
, et CommissionPolicy
are a PayrollPolicy
.
SalaryPolicy
is initialized with a weekly_salary
value that is then used in .calculate_payroll()
. HourlyPolicy
is initialized with the hour_rate
, and implements .calculate_payroll()
by leveraging the base class hours_worked
.
le CommissionPolicy
class derives from SalaryPolicy
because it wants to inherit its implementation. It is initialized with the weekly_salary
parameters, but it also requires a commission_per_sale
parameter.
le commission_per_sale
is used to calculate the .commission
, which is implemented as a property so it gets calculated when requested. In the example, we are assuming that a sale happens every 5 hours worked, and the .commission
is the number of sales times the commission_per_sale
value.
CommissionPolicy
implements the .calculate_payroll()
method by first leveraging the implementation in SalaryPolicy
and then adding the calculated commission.
You can now add an AddressBook
class to manage employee addresses:
# In contacts.py
classe AddressBook:
def __init__(soi):
soi._employee_addresses =
1: Adresse('121 Admin Rd.', 'Concord', 'NH', '03301'),
2: Adresse('67 Paperwork Ave', 'Manchester', 'NH', '03101'),
3: Adresse('15 Rose St', 'Concord', 'NH', '03301', 'Apt. B-1'),
4: Adresse('39 Sole St.', 'Concord', 'NH', '03301'),
5: Adresse('99 Mountain Rd.', 'Concord', 'NH', '03301'),
def get_employee_address(soi, employee_id):
adresse = soi._employee_addresses.obtenir(employee_id)
si ne pas adresse:
élever ValueError(employee_id)
revenir adresse
le AddressBook
class keeps an internal database of Adresse
objects for each employee. It exposes a get_employee_address()
method that returns the address of the specified employee identifiant
. If the employee identifiant
doesn’t exist, then it raises a ValueError
.
le Adresse
class implementation remains the same as before:
# In contacts.py
classe Adresse:
def __init__(soi, rue, ville, Etat, zipcode, street2=''):
soi.rue = rue
soi.street2 = street2
soi.ville = ville
soi.Etat = Etat
soi.zipcode = zipcode
def __str__(soi):
lignes = [[[[soi.rue]
si soi.street2:
lignes.ajouter(soi.street2)
lignes.ajouter(F'self.city, self.state self.zipcode')
revenir ' n'.joindre(lignes)
The class manages the address components and provides a pretty representation of an address.
So far, the new classes have been extended to support more functionality, but there are no significant changes to the previous design. This is going to change with the design of the employés
module and its classes.
You can start by implementing an EmployeeDatabase
class:
# In employees.py
de productivité importation ProductivitySystem
de heure importation PayrollSystem
de contacts importation AddressBook
classe EmployeeDatabase:
def __init__(soi):
soi._employees = [[[[
'id': 1,
'name': 'Mary Poppins',
'role': 'manager'
,
'id': 2,
'name': 'John Smith',
'role': 'secretary'
,
'id': 3,
'name': 'Kevin Bacon',
'role': 'sales'
,
'id': 4,
'name': 'Jane Doe',
'role': 'factory'
,
'id': 5,
'name': 'Robin Williams',
'role': 'secretary'
,
]
soi.productivité = ProductivitySystem()
soi.paie = PayrollSystem()
soi.employee_addresses = AddressBook()
@property
def employés(soi):
revenir [[[[soi._create_employee(**Les données) pour Les données dans soi._employees]
def _create_employee(soi, identifiant, prénom, rôle):
adresse = soi.employee_addresses.get_employee_address(identifiant)
employee_role = soi.productivité.get_role(rôle)
payroll_policy = soi.paie.get_policy(identifiant)
revenir Employee(identifiant, prénom, adresse, employee_role, payroll_policy)
le EmployeeDatabase
keeps track of all the employees in the company. For each employee, it tracks the identifiant
, prénom
, et rôle
. Il a un instance of the ProductivitySystem
, le PayrollSystem
, et le AddressBook
. These instances are used to create employees.
It exposes an .employees
property that returns the list of employees. le Employee
objects are created in an internal method ._create_employee()
. Notice that you don’t have different types of Employee
classes. You just need to implement a single Employee
class:
# In employees.py
classe Employee:
def __init__(soi, identifiant, prénom, adresse, rôle, paie):
soi.identifiant = identifiant
soi.prénom = prénom
soi.adresse = adresse
soi.rôle = rôle
soi.paie = paie
def travail(soi, heures):
fonctions = soi.rôle.perform_duties(heures)
impression(F'Employee self.id - self.name:')
impression(F'- duties')
impression('')
soi.paie.track_work(heures)
def calculate_payroll(soi):
revenir soi.paie.calculate_payroll()
le Employee
class is initialized with the identifiant
, prénom
, et adresse
attributes. It also requires the productivity rôle
for the employee and the paie
politique.
The class exposes a .work()
method that takes the hours worked. This method first retrieves the fonctions
du rôle
. In other words, it delegates to the rôle
object to perform its duties.
In the same way, it delegates to the paie
object to track the work heures
. le paie
, as you saw, uses those hours to calculate the payroll if needed.
The following diagram shows the composition design used:
The diagram shows the design of composition based policies. There is a single Employee
that is composed of other data objects like Adresse
and depends on the IRole
et IPayrollCalculator
interfaces to delegate the work. There are multiple implementations of these interfaces.
You can now use this design in your program:
# In program.py
de heure importation PayrollSystem
de productivité importation ProductivitySystem
de employés importation EmployeeDatabase
productivity_system = ProductivitySystem()
payroll_system = PayrollSystem()
employee_database = EmployeeDatabase()
employés = employee_database.employés
productivity_system.Piste(employés, 40)
payroll_system.calculate_payroll(employés)
You can run the program to see its output:
$ python program.py
Tracking Employee Productivity
==============================
Employee 1 - Mary Poppins:
- screams and yells for 40 hours.
Employee 2 - John Smith:
- does paperwork for 40 hours.
Employee 3 - Kevin Bacon:
- expends 40 hours on the phone.
Employee 4 - Jane Doe:
- manufactures gadgets for 40 hours.
Employee 5 - Robin Williams:
- does paperwork for 40 hours.
Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000
- Sent to:
121 Admin Rd.
Concord, NH 03301
Payroll for: 2 - John Smith
- Check amount: 1500
- Sent to:
67 Paperwork Ave
Manchester, NH 03101
Payroll for: 3 - Kevin Bacon
- Check amount: 1800.0
- Sent to:
15 Rose St
Apt. B-1
Concord, NH 03301
Payroll for: 4 - Jane Doe
- Check amount: 600
- Sent to:
39 Sole St.
Concord, NH 03301
Payroll for: 5 - Robin Williams
- Check amount: 360
- Sent to:
99 Mountain Rd.
Concord, NH 03301
This design is what is called policy-based design, where classes are composed of policies, and they delegate to those policies to do the work.
Policy-based design was introduced in the book Modern C++ Design, and it uses template metaprogramming in C++ to achieve the results.
Python does not support templates, but you can achieve similar results using composition, as you saw in the example above.
This type of design gives you all the flexibility you’ll need as requirements change. Imagine you need to change the way payroll is calculated for an object at run-time.
Customizing Behavior With Composition
If your design relies on inheritance, you need to find a way to change the type of an object to change its behavior. With composition, you just need to change the policy the object uses.
Imagine that our directeur
all of a sudden becomes a temporary employee that gets paid by the hour. You can modify the object during the execution of the program in the following way:
# In program.py
de heure importation PayrollSystem, HourlyPolicy
de productivité importation ProductivitySystem
de employés importation EmployeeDatabase
productivity_system = ProductivitySystem()
payroll_system = PayrollSystem()
employee_database = EmployeeDatabase()
employés = employee_database.employés
directeur = employés[[[[0]
directeur.paie = HourlyPolicy(55)
productivity_system.Piste(employés, 40)
payroll_system.calculate_payroll(employés)
The program gets the employee list from the EmployeeDatabase
and retrieves the first employee, which is the manager we want. Then it creates a new HourlyPolicy
initialized at $55 per hour and assigns it to the manager object.
The new policy is now used by the PayrollSystem
modifying the existing behavior. You can run the program again to see the result:
$ python program.py
Tracking Employee Productivity
==============================
Employee 1 - Mary Poppins:
- screams and yells for 40 hours.
Employee 2 - John Smith:
- does paperwork for 40 hours.
Employee 3 - Kevin Bacon:
- expends 40 hours on the phone.
Employee 4 - Jane Doe:
- manufactures gadgets for 40 hours.
Employee 5 - Robin Williams:
- does paperwork for 40 hours.
Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 2200
- Sent to:
121 Admin Rd.
Concord, NH 03301
Payroll for: 2 - John Smith
- Check amount: 1500
- Sent to:
67 Paperwork Ave
Manchester, NH 03101
Payroll for: 3 - Kevin Bacon
- Check amount: 1800.0
- Sent to:
15 Rose St
Apt. B-1
Concord, NH 03301
Payroll for: 4 - Jane Doe
- Check amount: 600
- Sent to:
39 Sole St.
Concord, NH 03301
Payroll for: 5 - Robin Williams
- Check amount: 360
- Sent to:
99 Mountain Rd.
Concord, NH 03301
The check for Mary Poppins, our manager, is now for $2200 instead of the fixed salary of $3000 that she had per week.
Notice how we added that business rule to the program without changing any of the existing classes. Consider what type of changes would’ve been required with an inheritance design.
You would’ve had to create a new class and change the type of the manager employee. There is no chance you could’ve changed the policy at run-time.
Choosing Between Inheritance and Composition in Python
So far, you’ve seen how inheritance and composition work in Python. You’ve seen that derived classes inherit the interface and implementation of their base classes. You’ve also seen that composition allows you to reuse the implementation of another class.
You’ve implemented two solutions to the same problem. The first solution used multiple inheritance, and the second one used composition.
You’ve also seen that Python’s duck typing allows you to reuse objects with existing parts of a program by implementing the desired interface. In Python, it isn’t necessary to derive from a base class for your classes to be reused.
At this point, you might be asking when to use inheritance vs composition in Python. They both enable code reuse. Inheritance and composition can tackle similar problems in your Python programs.
The general advice is to use the relationship that creates fewer dependencies between two classes. This relation is composition. Still, there will be times where inheritance will make more sense.
The following sections provide some guidelines to help you make the right choice between inheritance and composition in Python.
Inheritance to Model “Is A” Relationship
Inheritance should only be used to model an est un relationship. Liskov’s substitution principle says that an object of type Derived
, which inherits from Base
, can replace an object of type Base
without altering the desirable properties of a program.
Liskov’s substitution principle is the most important guideline to determine if inheritance is the appropriate design solution. Still, the answer might not be straightforward in all situations. Fortunately, there is a simple test you can use to determine if your design follows Liskov’s substitution principle.
Let’s say you have a class UNE
that provides an implementation and interface you want to reuse in another class B
. Your initial thought is that you can derive B
de UNE
and inherit both the interface and implementation. To be sure this is the right design, you follow theses steps:
-
Evaluate
B
est unUNE
: Think about this relationship and justify it. Does it make sense? -
Evaluate
UNE
est unB
: Reverse the relationship and justify it. Does it also make sense?
If you can justify both relationships, then you should never inherit those classes from one another. Let’s look at a more concrete example.
You have a class Rectangle
which exposes an .area
propriété. You need a class Carré
, which also has an .area
. It seems that a Carré
is a special type of Rectangle
, so maybe you can derive from it and leverage both the interface and implementation.
Before you jump into the implementation, you use Liskov’s substitution principle to evaluate the relationship.
UNE Carré
est un Rectangle
because its area is calculated from the product of its la taille
times its longueur
. The constraint is that Square.height
et Square.length
must be equal.
It makes sense. You can justify the relationship and explain why a Carré
est un Rectangle
. Let’s reverse the relationship to see if it makes sense.
UNE Rectangle
est un Carré
because its area is calculated from the product of its la taille
times its longueur
. The difference is that Rectangle.height
et Rectangle.width
can change independently.
It also makes sense. You can justify the relationship and describe the special constraints for each class. This is a good sign that these two classes should never derive from each other.
You might have seen other examples that derive Carré
de Rectangle
to explain inheritance. You might be skeptical with the little test you just did. Fair enough. Let’s write a program that illustrates the problem with deriving Carré
de Rectangle
.
First, you implement Rectangle
. You’re even going to encapsulate the attributes to ensure that all the constraints are met:
# In rectangle_square_demo.py
classe Rectangle:
def __init__(soi, longueur, la taille):
soi._length = longueur
soi._height = la taille
@property
def surface(soi):
revenir soi._length * soi._height
le Rectangle
class is initialized with a longueur
et un la taille
, and it provides an .area
property that returns the area. le longueur
et la taille
are encapsulated to avoid changing them directly.
Now, you derive Carré
de Rectangle
and override the necessary interface to meet the constraints of a Carré
:
# In rectangle_square_demo.py
classe Carré(Rectangle):
def __init__(soi, side_size):
super().__init__(side_size, side_size)
le Carré
class is initialized with a side_size
, which is used to initialize both components of the base class. Now, you write a small program to test the behavior:
# In rectangle_square_demo.py
rectangle = Rectangle(2, 4)
affirmer rectangle.surface == 8
carré = Carré(2)
affirmer carré.surface == 4
impression('OK!')
The program creates a Rectangle
et un Carré
and asserts that their .area
is calculated correctly. You can run the program and see that everything is OK
so far:
$ python rectangle_square_demo.py
D'ACCORD!
The program executes correctly, so it seems that Carré
is just a special case of a Rectangle
.
Later on, you need to support resizing Rectangle
objects, so you make the appropriate changes to the class:
# In rectangle_square_demo.py
classe Rectangle:
def __init__(soi, longueur, la taille):
soi._length = longueur
soi._height = la taille
@property
def surface(soi):
revenir soi._length * soi._height
def redimensionner(soi, new_length, new_height):
soi._length = new_length
soi._height = new_height
.resize()
takes the new_length
et new_width
for the object. You can add the following code to the program to verify that it works correctly:
# In rectangle_square_demo.py
rectangle.redimensionner(3, 5)
affirmer rectangle.surface == 15
impression('OK!')
You resize the rectangle object and assert that the new area is correct. You can run the program to verify the behavior:
$ python rectangle_square_demo.py
D'ACCORD!
The assertion passes, and you see that the program runs correctly.
So, what happens if you resize a square? Modify the program, and try to modify the carré
object:
# In rectangle_square_demo.py
carré.redimensionner(3, 5)
impression(F'Square area: square.area')
You pass the same parameters to square.resize()
that you used with rectangle
, and print the area. When you run the program you see:
$ python rectangle_square_demo.py
Square area: 15
D'ACCORD!
The program shows that the new area is 15
comme le rectangle
object. The problem now is that the carré
object no longer meets the Carré
class constraint that the longueur
et la taille
must be equal.
How can you fix that problem? You can try several approaches, but all of them will be awkward. You can override .resize()
dans carré
and ignore the la taille
parameter, but that will be confusing for people looking at other parts of the program where rectangles
are being resized and some of them are not getting the expected areas because they are really des carrés
.
In a small program like this one, it might be easy to spot the causes of the weird behavior, but in a more complex program, the problem will be harder to find.
The reality is that if you’re able to justify an inheritance relationship between two classes both ways, you should not derive one class from another.
In the example, it doesn’t make sense that Carré
inherits the interface and implementation of .resize()
de Rectangle
. That doesn’t mean that Carré
objects can’t be resized. It means that the interface is different because it only needs a side_size
parameter.
This difference in interface justifies not deriving Carré
de Rectangle
like the test above advised.
Mixing Features With Mixin Classes
One of the uses of multiple inheritance in Python is to extend a class features through mixins. UNE mixin is a class that provides methods to other classes but are not considered a base class.
A mixin allows other classes to reuse its interface and implementation without becoming a super class. They implement a unique behavior that can be aggregated to other unrelated classes. They are similar to composition but they create a stronger relationship.
Let’s say you want to convert objects of certain types in your application to a dictionary representation of the object. You could provide a .to_dict()
method in every class that you want to support this feature, but the implementation of .to_dict()
seems to be very similar.
This could be a good candidate for a mixin. You start by slightly modifying the Employee
class from the composition example:
# In employees.py
classe Employee:
def __init__(soi, identifiant, prénom, adresse, rôle, paie):
soi.identifiant = identifiant
soi.prénom = prénom
soi.adresse = adresse
soi._role = rôle
soi._payroll = paie
def travail(soi, heures):
fonctions = soi._role.perform_duties(heures)
impression(F'Employee self.id - self.name:')
impression(F'- duties')
impression('')
soi._payroll.track_work(heures)
def calculate_payroll(soi):
revenir soi._payroll.calculate_payroll()
The change is very small. You just changed the rôle
et paie
attributes to be internal by adding a leading underscore to their name. You will see soon why you are making that change.
Now, you add the AsDictionaryMixin
class:
# In representations.py
classe AsDictionaryMixin:
def to_dict(soi):
revenir
soutenir: soi._represent(valeur)
pour soutenir, valeur dans soi.__dict__.articles()
si ne pas soi._is_internal(soutenir)
def _represent(soi, valeur):
si isinstance(valeur, objet):
si hasattr(valeur, 'to_dict'):
revenir valeur.to_dict()
autre:
revenir str(valeur)
autre:
revenir valeur
def _is_internal(soi, soutenir):
revenir soutenir.startswith('_')
le AsDictionaryMixin
class exposes a .to_dict()
method that returns the representation of itself as a dictionary. The method is implemented as a dict
comprehension that says, “Create a dictionary mapping soutenir
à valeur
for each item in self.__dict__.items()
si la soutenir
is not internal.”
Remarque: This is why we made the role and payroll attributes internal in the Employee
class, because we don’t want to represent them in the dictionary.
As you saw at the beginning, creating a class inherits some members from objet
, and one of those members is __dict__
, which is basically a mapping of all the attributes in an object to their value.
You iterate through all the items in __dict__
and filter out the ones that have a name that starts with an underscore using ._is_internal()
.
._represent()
checks the specified value. If the value est un objet
, then it looks to see if it also has a .to_dict()
member and uses it to represent the object. Otherwise, it returns a string representation. If the value is not an objet
, then it simply returns the value.
You can modify the Employee
class to support this mixin:
# In employees.py
de représentations importation AsDictionaryMixin
classe Employee(AsDictionaryMixin):
def __init__(soi, identifiant, prénom, adresse, rôle, paie):
soi.identifiant = identifiant
soi.prénom = prénom
soi.adresse = adresse
soi._role = rôle
soi._payroll = paie
def travail(soi, heures):
fonctions = soi._role.perform_duties(heures)
impression(F'Employee self.id - self.name:')
impression(F'- duties')
impression('')
soi._payroll.track_work(heures)
def calculate_payroll(soi):
revenir soi._payroll.calculate_payroll()
All you have to do is inherit the AsDictionaryMixin
to support the functionality. It will be nice to support the same functionality in the Adresse
class, so the Employee.address
attribute is represented in the same way:
# In contacts.py
de représentations importation AsDictionaryMixin
classe Adresse(AsDictionaryMixin):
def __init__(soi, rue, ville, Etat, zipcode, street2=''):
soi.rue = rue
soi.street2 = street2
soi.ville = ville
soi.Etat = Etat
soi.zipcode = zipcode
def __str__(soi):
lignes = [[[[soi.rue]
si soi.street2:
lignes.ajouter(soi.street2)
lignes.ajouter(F'self.city, self.state self.zipcode')
revenir ' n'.joindre(lignes)
You apply the mixin to the Adresse
class to support the feature. Now, you can write a small program to test it:
# In program.py
importation json
de employés importation EmployeeDatabase
def print_dict(ré):
impression(json.décharges(ré, retrait=2))
pour employé dans EmployeeDatabase().employés:
print_dict(employé.to_dict())
The program implements a print_dict()
that converts the dictionary to a JSON string using indentation so the output looks better.
Then, it iterates through all the employees, printing the dictionary representation provided by .to_dict()
. You can run the program to see its output:
$ python program.py
"id": "1",
"name": "Mary Poppins",
"address":
"street": "121 Admin Rd.",
"street2": "",
"city": "Concord",
"state": "NH",
"zipcode": "03301"
"id": "2",
"name": "John Smith",
"address":
"street": "67 Paperwork Ave",
"street2": "",
"city": "Manchester",
"state": "NH",
"zipcode": "03101"
"id": "3",
"name": "Kevin Bacon",
"address":
"street": "15 Rose St",
"street2": "Apt. B-1",
"city": "Concord",
"state": "NH",
"zipcode": "03301"
"id": "4",
"name": "Jane Doe",
"address":
"street": "39 Sole St.",
"street2": "",
"city": "Concord",
"state": "NH",
"zipcode": "03301"
"id": "5",
"name": "Robin Williams",
"address":
"street": "99 Mountain Rd.",
"street2": "",
"city": "Concord",
"state": "NH",
"zipcode": "03301"
You leveraged the implementation of AsDictionaryMixin
à la fois Employee
et Adresse
classes even when they are not related. Parce que AsDictionaryMixin
only provides behavior, it is easy to reuse with other classes without causing problems.
Composition to Model “Has A” Relationship
Composition models a a un relationship. With composition, a class Composite
a un instance of class Composant
and can leverage its implementation. le Composant
class can be reused in other classes completely unrelated to the Composite
.
In the composition example above, the Employee
classe a un Adresse
object. Adresse
implements all the functionality to handle addresses, and it can be reused by other classes.
Other classes like Client
ou Vendor
can reuse Adresse
without being related to Employee
. They can leverage the same implementation ensuring that addresses are handled consistently across the application.
A problem you may run into when using composition is that some of your classes may start growing by using multiple components. Your classes may require multiple parameters in the constructor just to pass in the components they are made of. This can make your classes hard to use.
A way to avoid the problem is by using the Factory Method to construct your objects. You did that with the composition example.
If you look at the implementation of the EmployeeDatabase
class, you’ll notice that it uses ._create_employee()
to construct an Employee
object with the right parameters.
This design will work, but ideally, you should be able to construct an Employee
object just by specifying an identifiant
, for example employee = Employee(1)
.
The following changes might improve your design. You can start with the productivité
module:
# In productivity.py
classe _ProductivitySystem:
def __init__(soi):
soi._roles =
'manager': ManagerRole,
'secretary': SecretaryRole,
'sales': SalesRole,
'factory': FactoryRole,
def get_role(soi, role_id):
role_type = soi._roles.obtenir(role_id)
si ne pas role_type:
élever ValueError('role_id')
revenir role_type()
def Piste(soi, employés, heures):
impression('Tracking Employee Productivity')
impression('==============================')
pour employé dans employés:
employé.travail(heures)
impression('')
# Role classes implementation omitted
_productivity_system = _ProductivitySystem()
def get_role(role_id):
revenir _productivity_system.get_role(role_id)
def Piste(employés, heures):
_productivity_system.Piste(employés, heures)
First, you make the _ProductivitySystem
class internal, and then provide a _productivity_system
internal variable to the module. You are communicating to other developers that they should not create or use the _ProductivitySystem
directly. Instead, you provide two functions, get_role()
et track()
, as the public interface to the module. This is what other modules should use.
What you are saying is that the _ProductivitySystem
is a Singleton, and there should only be one object created from it.
Now, you can do the same with the heure
module:
# In hr.py
classe _PayrollSystem:
def __init__(soi):
soi._employee_policies =
1: SalaryPolicy(3000),
2: SalaryPolicy(1500),
3: CommissionPolicy(1000, 100),
4: HourlyPolicy(15),
5: HourlyPolicy(9)
def get_policy(soi, employee_id):
politique = soi._employee_policies.obtenir(employee_id)
si ne pas politique:
revenir ValueError(employee_id)
revenir politique
def calculate_payroll(soi, employés):
impression('Calculating Payroll')
impression('===================')
pour employé dans employés:
impression(F'Payroll for: employee.id - employee.name')
impression(F'- Check amount: employee.calculate_payroll()')
si employé.adresse:
impression('- Sent to:')
impression(employé.adresse)
impression('')
# Policy classes implementation omitted
_payroll_system = _PayrollSystem()
def get_policy(employee_id):
revenir _payroll_system.get_policy(employee_id)
def calculate_payroll(employés):
_payroll_system.calculate_payroll(employés)
Again, you make the _PayrollSystem
internal and provide a public interface to it. The application will use the public interface to get policies and calculate payroll.
You will now do the same with the contacts
module:
# In contacts.py
classe _AddressBook:
def __init__(soi):
soi._employee_addresses =
1: Adresse('121 Admin Rd.', 'Concord', 'NH', '03301'),
2: Adresse('67 Paperwork Ave', 'Manchester', 'NH', '03101'),
3: Adresse('15 Rose St', 'Concord', 'NH', '03301', 'Apt. B-1'),
4: Adresse('39 Sole St.', 'Concord', 'NH', '03301'),
5: Adresse('99 Mountain Rd.', 'Concord', 'NH', '03301'),
def get_employee_address(soi, employee_id):
adresse = soi._employee_addresses.obtenir(employee_id)
si ne pas adresse:
élever ValueError(employee_id)
revenir adresse
# Implementation of Address class omitted
_address_book = _AddressBook()
def get_employee_address(employee_id):
revenir _address_book.get_employee_address(employee_id)
You are basically saying that there should only be one _AddressBook
, one _PayrollSystem
, and one _ProductivitySystem
. Again, this design pattern is called the Singleton design pattern, which comes in handy for classes from which there should only be one, single instance.
Now, you can work on the employés
module. You will also make a Singleton out of the _EmployeeDatabase
, but you will make some additional changes:
# In employees.py
de productivité importation get_role
de heure importation get_policy
de contacts importation get_employee_address
de représentations importation AsDictionaryMixin
classe _EmployeeDatabase:
def __init__(soi):
soi._employees =
1:
'name': 'Mary Poppins',
'role': 'manager'
,
2:
'name': 'John Smith',
'role': 'secretary'
,
3:
'name': 'Kevin Bacon',
'role': 'sales'
,
4:
'name': 'Jane Doe',
'role': 'factory'
,
5:
'name': 'Robin Williams',
'role': 'secretary'
@property
def employés(soi):
revenir [[[[Employee(id_) pour id_ dans triés(soi._employees)]
def get_employee_info(soi, employee_id):
info = soi._employees.obtenir(employee_id)
si ne pas info:
élever ValueError(employee_id)
revenir info
classe Employee(AsDictionaryMixin):
def __init__(soi, identifiant):
soi.identifiant = identifiant
info = employee_database.get_employee_info(soi.identifiant)
soi.prénom = info.obtenir('name')
soi.adresse = get_employee_address(soi.identifiant)
soi._role = get_role(info.obtenir('role'))
soi._payroll = get_policy(soi.identifiant)
def travail(soi, heures):
fonctions = soi._role.perform_duties(heures)
impression(F'Employee self.id - self.name:')
impression(F'- duties')
impression('')
soi._payroll.track_work(heures)
def calculate_payroll(soi):
revenir soi._payroll.calculate_payroll()
employee_database = _EmployeeDatabase()
You first import the relevant functions and classes from other modules. le _EmployeeDatabase
is made internal, and at the bottom, you create a single instance. This instance is public and part of the interface because you will want to use it in the application.
You changed the _EmployeeDatabase._employees
attribute to be a dictionary where the key is the employee identifiant
and the value is the employee information. You also exposed a .get_employee_info()
method to return the information for the specified employee employee_id
.
le _EmployeeDatabase.employees
property now sorts the keys to return the employees sorted by their identifiant
. You replaced the method that constructed the Employee
objects with calls to the Employee
initializer directly.
le Employee
class now is initialized with the identifiant
and uses the public functions exposed in the other modules to initialize its attributes.
You can now change the program to test the changes:
# In program.py
importation json
de heure importation calculate_payroll
de productivité importation Piste
de employés importation employee_database, Employee
def print_dict(ré):
impression(json.décharges(ré, retrait=2))
employés = employee_database.employés
Piste(employés, 40)
calculate_payroll(employés)
temp_secretary = Employee(5)
impression('Temporary Secretary:')
print_dict(temp_secretary.to_dict())
You import the relevant functions from the heure
et productivité
modules, as well as the employee_database
et Employee
class. The program is cleaner because you exposed the required interface and encapsulated how objects are accessed.
Notice that you can now create an Employee
object directly just using its identifiant
. You can run the program to see its output:
$ python program.py
Tracking Employee Productivity
==============================
Employee 1 - Mary Poppins:
- screams and yells for 40 hours.
Employee 2 - John Smith:
- does paperwork for 40 hours.
Employee 3 - Kevin Bacon:
- expends 40 hours on the phone.
Employee 4 - Jane Doe:
- manufactures gadgets for 40 hours.
Employee 5 - Robin Williams:
- does paperwork for 40 hours.
Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000
- Sent to:
121 Admin Rd.
Concord, NH 03301
Payroll for: 2 - John Smith
- Check amount: 1500
- Sent to:
67 Paperwork Ave
Manchester, NH 03101
Payroll for: 3 - Kevin Bacon
- Check amount: 1800.0
- Sent to:
15 Rose St
Apt. B-1
Concord, NH 03301
Payroll for: 4 - Jane Doe
- Check amount: 600
- Sent to:
39 Sole St.
Concord, NH 03301
Payroll for: 5 - Robin Williams
- Check amount: 360
- Sent to:
99 Mountain Rd.
Concord, NH 03301
Temporary Secretary:
"id": "5",
"name": "Robin Williams",
"address":
"street": "99 Mountain Rd.",
"street2": "",
"city": "Concord",
"state": "NH",
"zipcode": "03301"
The program works the same as before, but now you can see that a single Employee
object can be created from its identifiant
and display its dictionary representation.
Take a closer look at the Employee
class:
# In employees.py
classe Employee(AsDictionaryMixin):
def __init__(soi, identifiant):
soi.identifiant = identifiant
info = employee_database.get_employee_info(soi.identifiant)
soi.prénom = info.obtenir('name')
soi.adresse = get_employee_address(soi.identifiant)
soi._role = get_role(info.obtenir('role'))
soi._payroll = get_policy(soi.identifiant)
def travail(soi, heures):
fonctions = soi._role.perform_duties(heures)
impression(F'Employee self.id - self.name:')
impression(F'- duties')
impression('')
soi._payroll.track_work(heures)
def calculate_payroll(soi):
revenir soi._payroll.calculate_payroll()
le Employee
class is a composite that contains multiple objects providing different functionality. It contains an Adresse
that implements all the functionality related to where the employee lives.
Employee
also contains a productivity role provided by the productivité
module, and a payroll policy provided by the heure
module. These two objects provide implementations that are leveraged by the Employee
class to track work in the .work()
method and to calculate the payroll in the .calculate_payroll()
méthode.
You are using composition in two different ways. le Adresse
class provides additional data to Employee
where the role and payroll objects provide additional behavior.
Still, the relationship between Employee
and those objects is loosely coupled, which provides some interesting capabilities that you’ll see in the next section.
Composition to Change Run-Time Behavior
Inheritance, as opposed to composition, is a tightly couple relationship. With inheritance, there is only one way to change and customize behavior. Method overriding is the only way to customize the behavior of a base class. This creates rigid designs that are difficult to change.
Composition, on the other hand, provides a loosely coupled relationship that enables flexible designs and can be used to change behavior at run-time.
Imagine you need to support a long-term disability (LTD) policy when calculating payroll. The policy states that an employee on LTD should be paid 60% of their weekly salary assuming 40 hours of work.
With an inheritance design, this can be a very difficult requirement to support. Adding it to the composition example is a lot easier. Let’s start by adding the policy class:
# In hr.py
classe LTDPolicy:
def __init__(soi):
soi._base_policy = None
def track_work(soi, heures):
soi._check_base_policy()
revenir soi._base_policy.track_work(heures)
def calculate_payroll(soi):
soi._check_base_policy()
base_salary = soi._base_policy.calculate_payroll()
revenir base_salary * 0.6
def apply_to_policy(soi, base_policy):
soi._base_policy = base_policy
def _check_base_policy(soi):
si ne pas soi._base_policy:
élever RuntimeError('Base policy missing')
Notice that LTDPolicy
doesn’t inherit PayrollPolicy
, but implements the same interface. This is because the implementation is completely different, so we don’t want to inherit any of the PayrollPolicy
implementation.
le LTDPolicy
initialise _base_policy
à None
, and provides an internal ._check_base_policy()
method that raises an exception if the ._base_policy
has not been applied. Then, it provides a .apply_to_policy()
method to assign the _base_policy
.
The public interface first checks that the _base_policy
has been applied, and then implements the functionality in terms of that base policy. le .track_work()
method just delegates to the base policy, and .calculate_payroll()
uses it to calculate the base_salary
and then return the 60%.
You can now make a small change to the Employee
class:
# In employees.py
classe Employee(AsDictionaryMixin):
def __init__(soi, identifiant):
soi.identifiant = identifiant
info = employee_database.get_employee_info(soi.identifiant)
soi.prénom = info.obtenir('name')
soi.adresse = get_employee_address(soi.identifiant)
soi._role = get_role(info.obtenir('role'))
soi._payroll = get_policy(soi.identifiant)
def travail(soi, heures):
fonctions = soi._role.perform_duties(heures)
impression(F'Employee self.id - self.name:')
impression(F'- duties')
impression('')
soi._payroll.track_work(heures)
def calculate_payroll(soi):
revenir soi._payroll.calculate_payroll()
def apply_payroll_policy(soi, new_policy):
new_policy.apply_to_policy(soi._payroll)
soi._payroll = new_policy
You added an .apply_payroll_policy()
method that applies the existing payroll policy to the new policy and then substitutes it. You can now modify the program to apply the policy to an Employee
object:
# In program.py
de heure importation calculate_payroll, LTDPolicy
de productivité importation Piste
de employés importation employee_database
employés = employee_database.employés
sales_employee = employés[[[[2]
ltd_policy = LTDPolicy()
sales_employee.apply_payroll_policy(ltd_policy)
Piste(employés, 40)
calculate_payroll(employés)
The program accesses sales_employee
, which is located at index 2
, creates the LTDPolicy
object, and applies the policy to the employee. Quand .calculate_payroll()
is called, the change is reflected. You can run the program to evaluate the output:
$ python program.py
Tracking Employee Productivity
==============================
Employee 1 - Mary Poppins:
- screams and yells for 40 hours.
Employee 2 - John Smith:
- Does paperwork for 40 hours.
Employee 3 - Kevin Bacon:
- Expends 40 hours on the phone.
Employee 4 - Jane Doe:
- Manufactures gadgets for 40 hours.
Employee 5 - Robin Williams:
- Does paperwork for 40 hours.
Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000
- Sent to:
121 Admin Rd.
Concord, NH 03301
Payroll for: 2 - John Smith
- Check amount: 1500
- Sent to:
67 Paperwork Ave
Manchester, NH 03101
Payroll for: 3 - Kevin Bacon
- Check amount: 1080.0
- Sent to:
15 Rose St
Apt. B-1
Concord, NH 03301
Payroll for: 4 - Jane Doe
- Check amount: 600
- Sent to:
39 Sole St.
Concord, NH 03301
Payroll for: 5 - Robin Williams
- Check amount: 360
- Sent to:
99 Mountain Rd.
Concord, NH 03301
The check amount for employee Kevin Bacon, who is the sales employee, is now for $1080 instead of $1800. That’s because the LTDPolicy
has been applied to the salary.
As you can see, you were able to support the changes just by adding a new policy and modifying a couple interfaces. This is the kind of flexibility that policy design based on composition gives you.
Choosing Between Inheritance and Composition in Python
Python, as an object oriented programming language, supports both inheritance and composition. You saw that inheritance is best used to model an est un relationship, whereas composition models a a un relationship.
Sometimes, it’s hard to see what the relationship between two classes should be, but you can follow these guidelines:
-
Use inheritance over composition in Python to model a clear est un relationship. First, justify the relationship between the derived class and its base. Then, reverse the relationship and try to justify it. If you can justify the relationship in both directions, then you should not use inheritance between them.
-
Use inheritance over composition in Python to leverage both the interface and implementation of the base class.
-
Use inheritance over composition in Python fournir mixin features to several unrelated classes when there is only one implementation of that feature.
-
Use composition over inheritance in Python to model a a un relationship that leverages the implementation of the component class.
-
Use composition over inheritance in Python to create components that can be reused by multiple classes in your Python applications.
-
Use composition over inheritance in Python to implement groups of behaviors and policies that can be applied interchangeably to other classes to customize their behavior.
-
Use composition over inheritance in Python to enable run-time behavior changes without affecting existing classes.
Conclusion
You explored inheritance and composition in Python. You learned about the type of relationships that inheritance and composition create. You also went through a series of exercises to understand how inheritance and composition are implemented in Python.
In this article, you learned how to:
- Use inheritance to express an est un relationship between two classes
- Evaluate if inheritance is the right relationship
- Use multiple inheritance in Python and evaluate Python’s MRO to troubleshoot multiple inheritance problems
- Extend classes with mixins and reuse their implementation
- Use composition to express a a un relationship between two classes
- Provide flexible designs using composition
- Reuse existing code through policy design based on composition
Recommended Reading
Here are some books and articles that further explore object oriented design and can be useful to help you understand the correct use of inheritance and composition in Python or other languages:
[ad_2]