Un guide Python – Real Python

By | août 7, 2019

Formation gratuite Python

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:

Héritage de base entre les classes de base et dérivées

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.

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:

Composition de base entre les classes Composite et Component

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 le Composite.
  • Le symbole indique que le Composite classe peut contenir un nombre variable de Composant les instances.
  • Une gamme 1..4 indique que le Composite classe peut contenir une gamme de Composant les instances. La plage est indiquée avec le nombre minimum et maximum d'instances, ou le nombre minimum d'instances, comme dans 1..*.

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.

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énomet 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:

Exemple d'héritage avec plusieurs classes dérivées Employee

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:

  1. Vous indiquez aux utilisateurs du module que des objets de type Employé ne peut pas être créé.
  2. Vous dites aux autres développeurs travaillant sur le heure module que s'ils dérivent de Employé, 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:

  1. 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.

  2. 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 un identifiant et un prénom, mais un Client n'est pas un Employé, 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:

Explosion de conception de classe par héritage

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:

  1. Tirer de secrétaire: Vous pouvez dériver de secrétaire hériter du .travail() méthode pour le rôle, puis remplacez la .calculate_payroll () méthode pour l'implémenter en tant que Salaire horaire.

  2. Tirer de Salaire horaire: Vous pouvez dériver de Salaire horaire hériter du .calculate_payroll () méthode, puis remplacez la .travail() méthode pour l'implémenter en tant que secré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:

  1. le TemporarySecretary.__init__(self, id, name, hours_worked, hour_rate) method is called.

  2. le super().__init__(id, name, hours_worked, hour_rate) call matches HourlyEmployee.__init__(self, id, name, hour_worked, hour_rate).

  3. HourlyEmployee appels super().__init__(id, name), which the MRO is going to match to Secretary.__init__(), which is inherited from SalaryEmployee.__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:

Diamond problem caused by multiple inheritance

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:

  1. The productivity system that tracks employee productivity.

  2. 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:

Policy based design using multiple inheritance

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:

  1. identifiant to identify an employee.
  2. 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:

Composition example with Employee containing Address

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:

Policy based design using composition

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:

  1. Evaluate B est un UNE: Think about this relationship and justify it. Does it make sense?

  2. Evaluate UNE est un B: 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.”

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():
    impression(json.décharges(, 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():
    impression(json.décharges(, 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

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: