Boostez vos cours avec Python super ()

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 fonctionsuper()

  • Fonctionnement de la fonctionsuper() en héritage simple

  • Fonctionnement de la fonctionsuper() 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 classeSquareinherited.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:

  1. Toutes les méthodes appelées avecsuper() 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.

  2. 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 as RightPyramid.__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

RightPyramid

base,slant_height

Square

length

base,height

Rectangle

length,width

base,height

Triangle

base,height

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: