HackInScience.org: Temperature Class

super c’est ce qui permet de faire apparaître explicitement le mécanisme du MRO en Python, afin de trouver le prochain parent lors de la résolution des attributs.
Mais ce mécanisme est en œuvre aussi sans super, c’est lui qui permet de retrouver dans quelle classe se trouve une méthode quand tu fais un obj.methode().

Je te remercie beaucoup car avec ces références j’ai regardé toutes ces notions.

Par contre, l’intérêt d’utiliser super() réside-t-il juste dans le fait d’écrire un code plus propre ?

Exemple:

Prenons 3 classes:

  • Rectangle,
  • Square : une version du rectangle,
  • Cube : la version 3D de Square.

Dans ce cas de figure on peut bénéficier du système d’héritage.

Dans le code ci-dessous j’utilise la version super et en commentaires la version ‘normale’, on voit bien qu’en utilisant super il y a moins de répétition et c’est plus concis.

Bon je réfléchis tout en écrivant, car à la base je me disais que c’était plus dur à maintenir, mais pas vraiment. Dans la version commentée, j’appelle une méthode d’une autre classe en spécifiant la classe (Rectangle.area()), ce qui est on ne peut plus explicite, alors que dans la version super, lorsqu’on voit ‘super()’, on va aller consulter la (ou les) superclasses pour vérifier la méthode utilisée.

Est-ce qu’il y a quelque chose qui m’échappe ou alors globalement… j’ai bon ?

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)

    def area(self):
        return super().area()

    # def __init__(self, length):
    #     self.length = length
    #     self.width = length

    # def area(self):
    #     return Rectangle.area(self)


class Cube(Square):
    def faces_area(self):
        face_area = super().area()
        return face_area * 6

    # def __init__(self, length):
    #     self.length = length
    #     self.width = length

    # def faces_area(self):
    #     return Rectangle.area(self) * 6


sq = Square(4)
print(sq.area())
cub = Cube(4)
print(cub.faces_area())

Tu peux même enlever :

car sans ça Python va trouver area du parent. Avec Python trouve ce area qui ne fait que … appeler le area du parent, donc ça revient au même.

Là ou super() est vraiment utile dans ton code c’est pour le __init__, c’est le seul moyen de passer les valeurs au Rectangle.

Pour montrer l’intérêt de super() hors d’un __init__ tu peux faire :

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def __repr__(self):
        return f"rectangle de {self.width}×{self.length}"

class RectangleColoré(Rectangle):
    def __init__(self, color, **kwargs):
        self.color = color
        super().__init__(**kwargs)

    def __repr__(self):
        return f"{super().__repr__()} de couleur {self.color}"

Qui s’utiliserai comme ça :

>>> Rectangle(length=1, width=2)
rectangle de 2×1
>>> RectangleColoré(length=1, width=2, color="rouge")
rectangle de 2×1 de couleur rouge

Note que j’ai utilisé **kwargs que tu peux lire « les autres arguments nommés » dans RectangleColoré pour transmettre aveuglément les arguments qui n’intéressent pas RectangleColoré à son parent. C’est très souvent une bonne idée car Rectangle du jour au lendemain peut accepter un nouvel argument, et tu n’auras pas à toucher au code de RectangleColoré pour le prendre en compte.

super() sert donc, quand il est utilisé dans une méthode à exprimer « cette méthode fait la même chose que la méthode de la classe parente, mais un peu différement ».

Dans RectangleColoré le super() dans le __repr__ sert à dire « La représentation d’un rectangle coloré c’est la même chose que la représentation d’un rectangle, mais un peu différente : avec la couleur à la fin »

2 « J'aime »

Non pas que.
On pourrait croire qu’un super().foo() pourrait juste être remplacé par Rectangle.foo(self), mais c’est en fait bien plus subtil que ça.

super va en effet te permettre d’atteindre des classes qui te seraient inconnues sans lui, parce qu’elles se trouvent dans le MRO mais que ce ne sont pas des classes parentes de la classe de départ.

Exemple :

class Base:
    def foo(self):
        print('avant')
        super().foo()
        print('après')

class A:
    def foo(self):
        print('coucou')

class B(Base, A):
    pass

B().foo()

Ici Base n’a aucune connaissance de la classe A, pourtant elle y fait appel via super.

3 « J'aime »

entwanne,
si tu enlève super dans ta class Base, à mon avis cela ne change rien.
Pour moi le MRO, remonte d’abord par les classes héritées les plus à gauche. C’est d’ailleurs pour cela que les mixin se positionnent à gauche.

Ah si ça change tout !

B hérite de Base avant d’hériter de A, donc B.foo correspond à Base.foo.

>>> B.foo
<function Base.foo at 0x7f50018a63a0>

Sans l’appel à super, A.foo ne serait donc jamais appelée.

C’est juste.

C’est d’ailleurs pour cela que les mixin se positionnent à gauche.

Exact.

Mais lorsque tu surcharge une méthode, aucun mécanisme implicite ne va appeler la méthode de même nom du parant dans la MRO, appeler super().la_methode() est le seul chemin pour aller exécuter le code de la_methode du suivant dans la MRO (c’est d’ailleurs super qui va aller la chercher dans la MRO, c’est son seul travail).

Pour revenir à « la MRO remonte d’abord par les classes héritées les plus à gauche » c’est tellement vrai que c’est garanti :

class A:
    ...


class B:
    ...


class BA(B, A):
    ...


class AB(A, B):
    ...


class C(BA, AB):
    ...

Ici la MRO de C a obligatoirement A avant B (via AB) et obligatoirement B avant A (via BA), ce qui est impossible, donc Python refuse de construire la classe C :

TypeError: Cannot create a consistent method resolution order (MRO) for bases B, A

Merci pour toutes vos réponses, je n’ai pas déserté mais, aléas de la vie faisant, j’ai dû mettre ce sujet de côté pendant quelques jours et j’y reviens d’ici le début de semaine prochaine. Car du coup, j’étais à deux doigts de trouver une solution que je comprenais pleinement et j’ai été interrompu en plein vol… donc on dira que, moteurs brusquement éteints, je suis en train de planner et on verra lorsque je reprend le sujet si je me plante ou si je continue le vol… héhé

En tout cas merci pour vos excellentes et enrichissantes contributions !

comment fait python, du coup, pour reconnaître quelle méthode foo appeler?

je suppose en préfixant foo par sa class?, mais dans ce cas lorsque deux méthodes portent le même nom elles sont toutes les deux appelées.
Auriez vous un cas d’usage en tête pour souhaitez faire quelque chose comme cela?
cela ressemble à un décorateur non?

Content que tu aies demandé !!!

Prenons cet exemple :

b = B()
b.foo()

TL;DR : Python va chercher dans la MRO du type de b le premier type ayant un nom “foo”. Le premier type ayant un nom “foo” sera le type utilisé pour appeler “foo”. Le boulot de l’interpréteur est terminé, c’est maintenant au dévelopeur, s’il le souhaite, d’utiliser super() pour “continuer le parcours de la MRO” (j’aime bien dire “coopérer”).

En plus long, b.foo() c’est une instruction CALL_METHOD, qui fonctionne en tandemme avec l’instruction LOAD_METHOD. LOAD_METHOD va appeler :

_PyObject_GetMethod(obj, name, ...) avec b et "foo" (source)

Pour trouver la méthode, _PyObject_GetMethod va chercher le nom foo dans le type B en appelant :

_PyType_Lookup(tp, name); (avec la classe B et la chaîne "foo") (source)

Qui va chercher quel type, dans la MRO, a un attribut de ce nom :

find_name_in_mro(type, name, &error);

qui se fait un parcours de la MRO :

n = PyTuple_GET_SIZE(mro);
for (i = 0; i < n; i++) {
    ...

mais dans ce cas lorsque deux méthodes portent le même nom elles sont toutes les deux appelées

Non, la première (d’un point de vue MRO) est appelée (celle de la classe trouvée par find_name_in_mro), et c’est à sa charge, si elle le souhaite ou non, d’appeler les “suivantes” (point de vue MRO) via super().

Et pour la magie derrière super, il faut savoir qu’un appel à super() dans une méthode est équivalent à super(__class__, self) (c’est fait implicitement par l’interpréteur).

Et donc super reçoit toutes les infos nécessaires pour résoudre l’appel : il sait dans quelle classe on est (__class__ qui pointe sur la classe courante dans un bloc class en Python), a donc accès à son MRO et à la classe suivante dans la liste, et il connaît l’instance courante (self) pour la passer à la méthode appelée ensuite (foo dans les exemples qui précédent).

1 « J'aime »

Merci pour toutes ces précisions!!
et quel serait un cas concret ou tu souhaites appeler un méthode portant le même nom, par un appel à super?

Bonjour,

Cet article en donne quelques exemples dans le meme style :

Et il y a plusieurs exemples dans ce code par exemple avec des vérifications, mise en cache ou ajouts de précisions par rapport au sous-class avant ou après avoir appelé la même méthode du class parent

Merci tous le monde!