Boostez vos cours avec Python super ()
Bien que Python ne soit pas uniquement un langage orienté objet, il est suffisamment flexible et puissant pour vous permettre de créer vos applications en utilisant le paradigme orienté objet. Une des façons dont Python y parvient est de prendre en chargeinheritance, ce qu'il fait avecsuper()
.
Dans ce didacticiel, vous découvrirez les éléments suivants:
-
Le concept d'héritage en Python
-
Héritage multiple en Python
-
Fonctionnement de la fonction
super()
-
Fonctionnement de la fonction
super()
en héritage simple -
Fonctionnement de la fonction
super()
en héritage multiple
Free Bonus:5 Thoughts On Python Mastery, un cours gratuit pour les développeurs Python qui vous montre la feuille de route et l'état d'esprit dont vous aurez besoin pour faire passer vos compétences Python au niveau supérieur.
Un aperçu de la fonctionsuper()
de Python
Si vous avez de l'expérience avec les langages orientés objet, vous connaissez peut-être déjà les fonctionnalités desuper()
.
Sinon, n’ayez pas peur! Alors que leofficial documentation est assez technique, à un niveau élevésuper()
vous donne accès aux méthodes d'une superclasse de la sous-classe qui en hérite.
super()
seul retourne un objet temporaire de la superclasse qui vous permet ensuite d'appeler les méthodes de cette superclasse.
Pourquoi voudriez-vous faire tout cela? Alors que les possibilités sont limitées par votre imagination, un cas d'utilisation courant est la construction de classes qui étendent les fonctionnalités des classes précédemment construites.
L'appel des méthodes précédemment construites avecsuper()
vous évite d'avoir à réécrire ces méthodes dans votre sous-classe, et vous permet d'échanger des superclasses avec des changements de code minimes.
super()
en héritage unique
Si vous ne connaissez pas les concepts de programmation orientée objet,inheritance peut être un terme inconnu. L'héritage est un concept de la programmation orientée objet dans lequel une classe dérive (ouinherits) des attributs et des comportements d'une autre classe sans avoir besoin de les implémenter à nouveau.
Pour moi au moins, il est plus facile de comprendre ces concepts lorsque l'on regarde le code, alors écrivons des classes décrivant certaines formes:
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
def perimeter(self):
return 2 * self.length + 2 * self.width
class Square:
def __init__(self, length):
self.length = length
def area(self):
return self.length * self.length
def perimeter(self):
return 4 * self.length
Ici, il existe deux classes similaires:Rectangle
etSquare
.
Vous pouvez les utiliser comme ci-dessous:
>>>
>>> square = Square(4)
>>> square.area()
16
>>> rectangle = Rectangle(2,4)
>>> rectangle.area()
8
Dans cet exemple, vous avez deux formes qui sont liées l'une à l'autre: un carré est un type spécial de rectangle. Le code, cependant, ne reflète pas cette relation et a donc un code qui est essentiellement répété.
En utilisant l'héritage, vous pouvez réduire la quantité de code que vous écrivez tout en reflétant simultanément la relation réelle entre les rectangles et les carrés:
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
def perimeter(self):
return 2 * self.length + 2 * self.width
# Here we declare that the Square class inherits from the Rectangle class
class Square(Rectangle):
def __init__(self, length):
super().__init__(length, length)
Ici, vous avez utilisésuper()
pour appeler les__init__()
de la classeRectangle
, ce qui vous permet de l’utiliser dans la classeSquare
sans répéter le code. Ci-dessous, la fonctionnalité principale reste après avoir apporté des modifications:
>>>
>>> square = Square(4)
>>> square.area()
16
Dans cet exemple,Rectangle
est la superclasse etSquare
est la sous-classe.
Parce que les méthodesSquare
etRectangle
.__init__()
sont si similaires, vous pouvez simplement appeler la méthode.__init__()
de la superclasse (Rectangle.__init__()
) à partir de celle deSquare
en utilisantsuper()
. Cela définit les attributs.length
et.width
même si vous avez juste dû fournir un seul paramètrelength
au constructeurSquare
.
Lorsque vous exécutez cela, même si votre classeSquare
ne l'implémente pas explicitement, l'appel à.area()
utilisera la méthode.area()
dans la superclasse et affichera16
. La classeSquare
inherited.area()
de la classeRectangle
.
Note: Pour en savoir plus sur l'héritage et les concepts orientés objet en Python, assurez-vous de consulterObject-Oriented Programming (OOP) in Python 3.
Que peut fairesuper()
pour vous?
Alors, que peut fairesuper()
pour vous en héritage unique?
Comme dans d'autres langages orientés objet, il vous permet d'appeler des méthodes de la superclasse dans votre sous-classe. Le cas d'utilisation principal de ceci est d'étendre les fonctionnalités de la méthode héritée.
Dans l'exemple ci-dessous, vous allez créer une classeCube
qui hérite deSquare
et étend la fonctionnalité de.area()
(héritée de la classeRectangle
viaSquare
) pour calculer la surface et le volume d'une instanceCube
:
class Square(Rectangle):
def __init__(self, length):
super().__init__(length, length)
class Cube(Square):
def surface_area(self):
face_area = super().area()
return face_area * 6
def volume(self):
face_area = super().area()
return face_area * self.length
Maintenant que vous avez créé les classes, examinons la surface et le volume d'un cube avec une longueur de côté de3
:
>>>
>>> cube = Cube(3)
>>> cube.surface_area()
54
>>> cube.volume()
27
Caution: Notez que dans notre exemple ci-dessus,super()
seul ne fera pas les appels de méthode pour vous: vous devez appeler la méthode sur l'objet proxy lui-même.
Ici, vous avez implémenté deux méthodes pour la classeCube
:.surface_area()
et.volume()
. Ces deux calculs reposent sur le calcul de l'aire d'une seule face, donc plutôt que de réimplémenter le calcul de l'aire, vous utilisezsuper()
pour étendre le calcul de l'aire.
Notez également que la définition de classeCube
n'a pas de.__init__()
. Étant donné queCube
hérite deSquare
et que.__init__()
ne fait rien différemment pourCube
qu'il ne le fait déjà pourSquare
, vous pouvez ignorer sa définition, et les.__init__()
de la superclasse (Square
) seront appelés automatiquement.
super()
renvoie un objet délégué à une classe parent, vous appelez donc directement la méthode que vous voulez dessus:super().area()
.
Non seulement cela nous évite d'avoir à réécrire les calculs de surface, mais cela nous permet également de changer la logique interne.area()
en un seul endroit. Ceci est particulièrement utile lorsque vous avez un certain nombre de sous-classes héritant d'une superclasse.
Une plongée approfondie desuper()
Avant de vous lancer dans l'héritage multiple, faisons un petit détour par la mécanique desuper()
.
Alors que les exemples ci-dessus (et ci-dessous) appellentsuper()
sans aucun paramètre,super()
peut également prendre deux paramètres: le premier est la sous-classe et le second paramètre est un objet qui est une instance de cette sous-classe.
Tout d'abord, voyons deux exemples montrant ce que la manipulation de la première variable peut faire, en utilisant les classes déjà affichées:
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
def perimeter(self):
return 2 * self.length + 2 * self.width
class Square(Rectangle):
def __init__(self, length):
super(Square, self).__init__(length, length)
Dans Python 3, l'appelsuper(Square, self)
équivaut à l'appelsuper()
sans paramètre. Le premier paramètre fait référence à la sous-classeSquare
, tandis que le second paramètre fait référence à un objetSquare
qui, dans ce cas, estself
. Vous pouvez également appelersuper()
avec d'autres classes:
class Cube(Square):
def surface_area(self):
face_area = super(Square, self).area()
return face_area * 6
def volume(self):
face_area = super(Square, self).area()
return face_area * self.length
Dans cet exemple, vous définissezSquare
comme argument de sous-classe sursuper()
, au lieu deCube
. Cela amènesuper()
à commencer la recherche d'une méthode correspondante (dans ce cas,.area()
) à un niveau supérieur àSquare
dans la hiérarchie des instances, dans ce casRectangle
.
Dans cet exemple spécifique, le comportement ne change pas. Mais imaginez queSquare
implémentait également une fonction.area()
que vous vouliez vous assurer queCube
n'utilisait pas. Appelersuper()
de cette manière vous permet de le faire.
Caution: Alors que nous faisons beaucoup de bidouillage avec les paramètres desuper()
afin d'explorer comment cela fonctionne sous le capot, je mets en garde contre le fait de le faire régulièrement.
L'appel sans paramètre àsuper()
est recommandé et suffisant pour la plupart des cas d'utilisation, et le besoin de modifier régulièrement la hiérarchie de recherche pourrait être le signe d'un problème de conception plus important.
Et le deuxième paramètre? N'oubliez pas qu'il s'agit d'un objet qui est une instance de la classe utilisée comme premier paramètre. Par exemple,isinstance(Cube, Square)
doit renvoyerTrue
.
En incluant un objet instancié,super()
renvoie unbound method: une méthode liée à l'objet, qui donne à la méthode le contexte de l'objet tel que les attributs d'instance. Si ce paramètre n'est pas inclus, la méthode renvoyée n'est qu'une fonction, non associée au contexte d'un objet.
Pour plus d'informations sur les méthodes liées, les méthodes indépendantes et les fonctions, lisez la documentation Pythonon its descriptor system.
Note: Techniquement,super()
ne renvoie pas de méthode. Il renvoie unproxy object. Il s'agit d'un objet qui délègue les appels aux méthodes de classe correctes sans créer d'objet supplémentaire pour ce faire.
super()
en héritage multiple
Maintenant que vous avez travaillé sur une présentation et quelques exemples desuper()
et d'héritage unique, vous serez présenté à une vue d'ensemble et à quelques exemples qui montreront comment fonctionne l'héritage multiple et commentsuper()
active cette fonctionnalité.
Présentation de l'héritage multiple
Il existe un autre cas d’utilisation dans lequelsuper()
brille vraiment, et celui-ci n’est pas aussi courant que le scénario d’héritage unique. Outre l'héritage unique, Python prend en charge l'héritage multiple, dans lequel une sous-classe peut hériter de plusieurs superclasses qui n'héritent pas nécessairement les unes des autres (également appeléessibling classes).
Je suis une personne très visuelle et je trouve que les diagrammes sont extrêmement utiles pour comprendre des concepts comme celui-ci. L'image ci-dessous montre un scénario d'héritage multiple très simple, où une classe hérite de deux superclasses non apparentées (frère):
Pour mieux illustrer l'héritage multiple en action, voici un code à essayer, montrant comment vous pouvez construire une pyramide droite (une pyramide à base carrée) à partir d'unTriangle
et d'unSquare
:
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
class RightPyramid(Triangle, Square):
def __init__(self, base, slant_height):
self.base = base
self.slant_height = slant_height
def area(self):
base_area = super().area()
perimeter = super().perimeter()
return 0.5 * perimeter * self.slant_height + base_area
Note: Le termeslant height peut ne pas vous être familier, surtout si cela fait longtemps que vous n'avez pas suivi un cours de géométrie ou travaillé sur des pyramides.
La hauteur oblique est la hauteur depuis le centre de la base d'un objet (comme une pyramide) jusqu'à sa face jusqu'au sommet de cet objet. Vous pouvez en savoir plus sur les hauteurs obliques àWolframMathWorld.
Cet exemple déclare une classeTriangle
et une classeRightPyramid
qui hérite à la fois deSquare
etTriangle
.
Vous verrez une autre méthode.area()
qui utilisesuper()
comme dans l'héritage simple, dans le but d'atteindre les méthodes.perimeter()
et.area()
définies tout en haut dans le ClasseRectangle
.
Note: Vous remarquerez peut-être que le code ci-dessus n'utilise pas encore de propriétés héritées de la classeTriangle
. Les exemples ultérieurs tireront pleinement parti de l'héritage deTriangle
etSquare
.
Le problème, cependant, est que les deux superclasses (Triangle
etSquare
) définissent un.area()
. Prenez une seconde et pensez à ce qui pourrait arriver lorsque vous appelez.area()
surRightPyramid
, puis essayez de l'appeler comme ci-dessous:
>>>
>> pyramid = RightPyramid(2, 4)
>> pyramid.area()
Traceback (most recent call last):
File "shapes.py", line 63, in
print(pyramid.area())
File "shapes.py", line 47, in area
base_area = super().area()
File "shapes.py", line 38, in area
return 0.5 * self.base * self.height
AttributeError: 'RightPyramid' object has no attribute 'height'
Avez-vous deviné que Python essaiera d'appelerTriangle.area()
? C'est à cause de quelque chose appelé lesmethod resolution order.
Note: Comment avons-nous remarqué queTriangle.area()
était appelé et non, comme nous l'espérions,Square.area()
? Si vous regardez la dernière ligne de la trace (avant lesAttributeError
), vous verrez une référence à une ligne de code spécifique:
return 0.5 * self.base * self.height
Vous pouvez reconnaître cela à partir de la classe de géométrie comme la formule de l'aire d'un triangle. Sinon, si vous êtes comme moi, vous avez peut-être fait défiler les définitions de classeTriangle
etRectangle
et vu ce même code dansTriangle.area()
.
Ordre de résolution des méthodes
L'ordre de résolution des méthodes (ouMRO) indique à Python comment rechercher les méthodes héritées. Ceci est pratique lorsque vous utilisezsuper()
car le MRO vous indique exactement où Python recherchera une méthode que vous appelez avecsuper()
et dans quel ordre.
Chaque classe a un attribut.__mro__
qui nous permet d'inspecter la commande, alors faisons-le:
>>>
>>> RightPyramid.__mro__
(, ,
, ,
)
Cela nous indique que les méthodes seront recherchées d'abord dansRightpyramid
, puis dansTriangle
, puis dansSquare
, puisRectangle
, et ensuite, si rien n'est trouvé, dansobject
, d'où proviennent toutes les classes.
Le problème ici est que l'interpréteur recherche.area()
dansTriangle
avantSquare
etRectangle
, et lors de la recherche de.area()
dansTriangle
, Python l'appelle au lieu de celui que vous voulez. CommeTriangle.area()
s'attend à ce qu'il y ait un attribut.height
et.base
, Python lance unAttributeError
.
Heureusement, vous avez un certain contrôle sur la façon dont le MRO est construit. En changeant simplement la signature de la classeRightPyramid
, vous pouvez rechercher dans l'ordre que vous voulez, et les méthodes se résoudront correctement:
class RightPyramid(Square, Triangle):
def __init__(self, base, slant_height):
self.base = base
self.slant_height = slant_height
super().__init__(self.base)
def area(self):
base_area = super().area()
perimeter = super().perimeter()
return 0.5 * perimeter * self.slant_height + base_area
Notez queRightPyramid
s'initialise partiellement avec les.__init__()
de la classeSquare
. Cela permet à.area()
d'utiliser les.length
sur l'objet, comme prévu.
Maintenant, vous pouvez construire une pyramide, inspecter le MRO et calculer la surface:
>>>
>>> pyramid = RightPyramid(2, 4)
>>> RightPyramid.__mro__
(, ,
, ,
)
>>> pyramid.area()
20.0
Vous voyez que le MRO est maintenant ce que vous attendez, et vous pouvez également inspecter la zone de la pyramide, grâce à.area()
et.perimeter()
.
Mais il y a toujours un problème ici. Par souci de simplicité, j'ai fait quelques erreurs dans cet exemple: la première, et sans doute le plus important, était que j'avais deux classes distinctes avec le même nom de méthode et la même signature.
Cela provoque des problèmes de résolution de méthode, car la première instance de.area()
rencontrée dans la liste MRO sera appelée.
Lorsque vous utilisezsuper()
avec héritage multiple, il est impératif de concevoir vos classes encooperate. Cela consiste en partie à garantir que vos méthodes sont uniques afin qu'elles soient résolues dans le MRO, en vous assurant que les signatures de méthode sont uniques, que ce soit en utilisant des noms de méthode ou des paramètres de méthode.
Dans ce cas, pour éviter une refonte complète de votre code, vous pouvez renommer la méthode.area()
de la classeTriangle
en.tri_area()
. De cette façon, les méthodes de zone peuvent continuer à utiliser les propriétés de classe plutôt que de prendre des paramètres externes:
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
super().__init__()
def tri_area(self):
return 0.5 * self.base * self.height
Allons aussi de l'avant et utilisons ceci dans la classeRightPyramid
:
class RightPyramid(Square, Triangle):
def __init__(self, base, slant_height):
self.base = base
self.slant_height = slant_height
super().__init__(self.base)
def area(self):
base_area = super().area()
perimeter = super().perimeter()
return 0.5 * perimeter * self.slant_height + base_area
def area_2(self):
base_area = super().area()
triangle_area = super().tri_area()
return triangle_area * 4 + base_area
Le problème suivant ici est que le code n'a pas d'objetTriangle
délégué comme il le fait pour un objetSquare
, donc appeler.area_2()
nous donnera unAttributeError
depuis.base
et.height
n'ont aucune valeur.
Vous devez faire deux choses pour résoudre ce problème:
-
Toutes les méthodes appelées avec
super()
doivent avoir un appel à la version de leur superclasse de cette méthode. Cela signifie que vous devrez ajoutersuper().__init__()
aux méthodes.__init__()
deTriangle
etRectangle
. -
Reconcevoir tous les appels
.__init__()
pour prendre un dictionnaire de mots-clés. Voir le code complet ci-dessous.
Il existe un certain nombre de différences importantes dans ce code:
-
kwargs
is modified in some places (such asRightPyramid.__init__()
): Cela permettra aux utilisateurs de ces objets de les instancier uniquement avec les arguments qui ont un sens pour cet objet particulier. -
Setting up named arguments before
**kwargs
: Vous pouvez le voir dansRightPyramid.__init__()
. Cela a pour effet de faire sortir cette clé du dictionnaire**kwargs
, de sorte qu'au moment où elle se termine à la fin du MRO dans la classeobject
,**kwargs
est vide.
Note: Suivre l'état dekwargs
peut être délicat ici, voici donc un tableau des appels de.__init__()
dans l'ordre, montrant la classe qui possède cet appel, et le contenu dekwargs
pendant cet appel:
Classe | Arguments nommés | kwargs |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
Maintenant, lorsque vous utilisez ces classes mises à jour, vous avez ceci:
>>>
>>> pyramid = RightPyramid(base=2, slant_height=4)
>>> pyramid.area()
20.0
>>> pyramid.area_2()
20.0
Ça marche! Vous avez utilisésuper()
pour naviguer avec succès dans une hiérarchie de classes compliquée tout en utilisant à la fois l'héritage et la composition pour créer de nouvelles classes avec une réimplémentation minimale.
Alternatives d'héritage multiples
Comme vous pouvez le voir, l'héritage multiple peut être utile mais aussi conduire à des situations très compliquées et à du code difficile à lire. Il est également rare d'avoir des objets qui héritent parfaitement de plus de plusieurs autres objets.
Si vous voyez que vous commencez à utiliser l'héritage multiple et une hiérarchie de classes compliquée, il vaut la peine de vous demander si vous pouvez obtenir un code plus propre et plus facile à comprendre en utilisantcomposition au lieu de l'héritage.
Avec la composition, vous pouvez ajouter des fonctionnalités très spécifiques à vos classes à partir d'une classe simple et spécialisée appeléemixin.
Étant donné que cet article est axé sur l'héritage, je n'entrerai pas trop dans les détails sur la composition et comment l'utiliser en Python, mais voici un court exemple utilisantVolumeMixin
pour donner des fonctionnalités spécifiques à nos objets 3D - dans ce cas , un calcul de volume:
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
class Square(Rectangle):
def __init__(self, length):
super().__init__(length, length)
class VolumeMixin:
def volume(self):
return self.area() * self.height
class Cube(VolumeMixin, Square):
def __init__(self, length):
super().__init__(length)
self.height = length
def face_area(self):
return super().area()
def surface_area(self):
return super().area() * 6
Dans cet exemple, le code a été retravaillé pour inclure un mixin appeléVolumeMixin
. Le mixin est ensuite utilisé parCube
et donne àCube
la possibilité de calculer son volume, comme illustré ci-dessous:
>>>
>>> cube = Cube(2)
>>> cube.surface_area()
24
>>> cube.volume()
8
Ce mixin peut être utilisé de la même manière dans toute classe qui a une zone définie pour lui et pour laquelle la formulearea * height
renvoie le volume correct.
Récapitulation Asuper()
Dans ce didacticiel, vous avez appris à suralimenter vos classes avecsuper()
. Votre voyage a commencé par un examen de l'héritage unique, puis a montré comment appeler facilement des méthodes de superclasse avecsuper()
.
Vous avez ensuite appris le fonctionnement de l'héritage multiple en Python et les techniques permettant de combinersuper()
avec l'héritage multiple. Vous avez également appris comment Python résout les appels de méthode à l'aide de l'ordre de résolution des méthodes (MRO), ainsi que comment inspecter et modifier le MRO pour garantir que les méthodes appropriées sont appelées aux moments appropriés.
Pour plus d'informations sur la programmation orientée objet en Python et l'utilisation desuper()
, consultez ces ressources: