Pourquoi Python utilise-t-il des «méthodes magiques»?

99

J'ai récemment joué avec Python, et une chose que je trouve un peu étrange est l'utilisation intensive de `` méthodes magiques '', par exemple pour rendre sa longueur disponible, un objet implémente une méthode, def __len__(self)puis il est appelé lorsque vous écrivez len(obj).

Je me demandais simplement pourquoi les objets ne définissent pas simplement une len(self)méthode et ne la font pas appeler directement en tant que membre de l'objet, par exemple obj.len()? Je suis sûr qu'il doit y avoir de bonnes raisons pour que Python le fasse comme il le fait, mais en tant que débutant, je n'ai pas encore compris ce qu'ils sont.

Greg Beech
la source
4
Je pense que la raison générale est a) historique et b) quelque chose comme len()ou reversed()s'applique à de nombreux types d'objets, mais une méthode telle que append()ne s'applique qu'aux séquences, etc.
Grant Paul

Réponses:

64

AFAIK, lenest spécial à cet égard et a des racines historiques.

Voici une citation de la FAQ :

Pourquoi Python utilise-t-il des méthodes pour certaines fonctionnalités (par exemple list.index ()) mais des fonctions pour d'autres (par exemple len (list))?

La raison principale est l'histoire. Les fonctions étaient utilisées pour les opérations qui étaient génériques pour un groupe de types et qui étaient destinées à fonctionner même pour des objets qui n'avaient pas du tout de méthode (par exemple des tuples). Il est également pratique d'avoir une fonction qui peut facilement être appliquée à une collection amorphe d'objets lorsque vous utilisez les fonctionnalités fonctionnelles de Python (map (), apply () et al).

En fait, implémenter len (), max (), min () en tant que fonction intégrée est en fait moins de code que de les implémenter en tant que méthodes pour chaque type. On peut ergoter sur des cas individuels, mais cela fait partie de Python, et il est trop tard pour apporter des changements aussi fondamentaux maintenant. Les fonctions doivent rester pour éviter une rupture massive du code.

Les autres «méthodes magiques» (en fait appelées méthode spéciale dans le folklore Python) ont beaucoup de sens, et des fonctionnalités similaires existent dans d'autres langages. Ils sont principalement utilisés pour le code qui est appelé implicitement lorsqu'une syntaxe spéciale est utilisée.

Par exemple:

  • opérateurs surchargés (existent en C ++ et autres)
  • constructeur / destructeur
  • hooks pour accéder aux attributs
  • outils de métaprogrammation

etc...

Eli Bendersky
la source
2
Python et le principe du moindre étonnement est une bonne lecture pour certains des avantages de Python étant de cette façon (bien que j'admets que l'anglais a besoin de travail). Le point de base: cela permet à la bibliothèque standard d'implémenter une tonne de code qui devient très, très réutilisable mais toujours remplaçable.
jpmc26
20

Du Zen de Python:

Face à l'ambiguïté, refusez la tentation de deviner.
Il devrait y avoir une - et de préférence une seule - façon évidente de le faire.

Ceci est l' une des raisons - avec des méthodes personnalisées, les développeurs seraient libres de choisir un nom de méthode différente, comme getLength(), length(), getlength()ou que ce soit. Python applique une dénomination stricte afin que la fonction commune len()puisse être utilisée.

Toutes les opérations qui sont communes pour de nombreux types d'objets sont mis en méthodes magiques, comme __nonzero__, __len__ou __repr__. Cependant, ils sont pour la plupart facultatifs.

La surcharge des opérateurs est également effectuée avec des méthodes magiques (par exemple __le__), il est donc logique de les utiliser également pour d'autres opérations courantes.

AndiDog
la source
C'est un argument convaincant. Plus satisfaisant que "Guido ne croyait pas vraiment en OO" .... (comme je l'ai vu prétendu ailleurs).
Andy Hayden
15

Python utilise le mot «méthodes magiques» , car ces méthodes font vraiment de la magie pour votre programme. L'un des plus grands avantages de l'utilisation des méthodes magiques de Python est qu'elles fournissent un moyen simple de faire en sorte que les objets se comportent comme des types intégrés. Cela signifie que vous pouvez éviter les méthodes laides, contre-intuitives et non standard d'exécuter des opérateurs de base.

Prenons l'exemple suivant:

dict1 = {1 : "ABC"}
dict2 = {2 : "EFG"}

dict1 + dict2
Traceback (most recent call last):
  File "python", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'dict' and 'dict'

Cela donne une erreur, car le type de dictionnaire ne prend pas en charge l'ajout. Maintenant, étendons la classe de dictionnaire et ajoutons la méthode magique "__add__" :

class AddableDict(dict):

    def __add__(self, otherObj):
        self.update(otherObj)
        return AddableDict(self)


dict1 = AddableDict({1 : "ABC"})
dict2 = AddableDict({2 : "EFG"})

print (dict1 + dict2)

Maintenant, il donne la sortie suivante.

{1: 'ABC', 2: 'EFG'}

Ainsi, en ajoutant cette méthode, la magie s'est soudainement produite et l'erreur que vous aviez plus tôt a disparu.

J'espère que cela vous rend les choses claires. Pour plus d'informations, reportez-vous à:

Un guide des méthodes magiques de Python (Rafe Kettler, 2012)

Mangu Singh Rajpurohit
la source
9

Certaines de ces fonctions font plus qu'une seule méthode pourrait implémenter (sans méthodes abstraites sur une superclasse). Par exemple, bool()agit un peu comme ceci:

def bool(obj):
    if hasattr(obj, '__nonzero__'):
        return bool(obj.__nonzero__())
    elif hasattr(obj, '__len__'):
        if obj.__len__():
            return True
        else:
            return False
    return True

Vous pouvez également être sûr à 100% que bool() retournera toujours Vrai ou Faux; si vous vous fiez à une méthode, vous ne pouviez pas être entièrement sûr de ce que vous obtiendriez.

Certaines autres fonctions qui ont des implémentations relativement compliquées (plus compliquées que les méthodes magiques sous-jacentes sont susceptibles de l'être) sont iter()et cmp(), et toutes les méthodes attributaires ( getattr, setattret delattr). Des choses comme intaccéder également aux méthodes magiques lors de la coercition (vous pouvez implémenter __int__), mais font double emploi en tant que types. len(obj)est en fait le seul cas dont je ne pense pas que ce soit jamais différent obj.__len__().

Ian Bicking
la source
2
Au lieu de hasattr()j'utiliserais try:/ except AttributeError:et au lieu de if obj.__len__(): return True else: return Falseje dirais simplement, return obj.__len__() > 0mais ce ne sont que des choses stylistiques.
Chris Lutz
En python 2.6 (auquel btw bool(x)faisait référence x.__nonzero__()), votre méthode ne fonctionnerait pas. Les instances booléennes ont une méthode __nonzero__()et votre code continuerait de s'appeler une fois que obj était un booléen. Devrait bool(obj.__bool__())peut-être être traité de la même manière que vous avez traité __len__? (Ou ce code fonctionne-t-il réellement pour Python 3?)
Ponkadoodle
La nature circulaire de bool () était quelque peu intentionnellement absurde, pour refléter la nature particulièrement circulaire de la définition. Il y a un argument selon lequel il devrait simplement être considéré comme un primitif.
Ian Bicking
La seule différence (actuellement) entre len(x)et x.__len__()est que le premier lèvera OverflowError pour les longueurs qui dépassent sys.maxsize, tandis que le dernier ne le fera généralement pas pour les types implémentés en Python. C'est plus un bogue qu'une fonctionnalité, cependant (par exemple, l'objet range de Python 3.2 peut principalement gérer des plages arbitrairement grandes, mais leur utilisation lenpeut échouer. Leur __len__échec également, car ils sont implémentés en C plutôt qu'en Python)
ncoghlan
4

Ce ne sont pas vraiment des "noms magiques". C'est juste l'interface qu'un objet doit implémenter pour fournir un service donné. En ce sens, ils ne sont pas plus magiques que n'importe quelle définition d'interface prédéfinie que vous devez réimplémenter.

Stefano Borini
la source
1

Bien que la raison soit principalement historique, il y a certaines particularités dans Python lenqui rendent l'utilisation d'une fonction au lieu d'une méthode appropriée.

Certaines opérations en Python sont implémentées en tant que méthodes, par exemple list.indexet dict.append, tandis que d'autres sont implémentées en tant que méthodes appelables et magiques, par exemple stret iteret reversed. Les deux groupes diffèrent suffisamment pour que l'approche différente soit justifiée:

  1. Ils sont communs.
  2. str, intet les amis sont des types. Il est plus logique d'appeler le constructeur.
  3. L'implémentation diffère de l'appel de fonction. Par exemple, iterpeut appeler __getitem__si __iter__n'est pas disponible et prend en charge des arguments supplémentaires qui ne rentrent pas dans un appel de méthode. Pour la même raison, it.next()a été changé ennext(it) les versions récentes de Python - cela a plus de sens.
  4. Certains d'entre eux sont des parents proches des exploitants. Il y a une syntaxe pour appeler __iter__et __next__- ça s'appelle la forboucle. Par souci de cohérence, une fonction est meilleure. Et cela le rend meilleur pour certaines optimisations.
  5. Certaines fonctions sont tout simplement trop similaires aux autres d'une certaine manière - repragissent comme strsi. Avoir str(x)contre x.repr()serait déroutant.
  6. Certains d'entre eux utilisent rarement la méthode de mise en œuvre réelle, par exemple isinstance.
  7. Certains d'entre eux sont de véritables opérateurs, getattr(x, 'a')c'est une autre façon de faire x.aet getattrpartagent plusieurs des qualités susmentionnées.

J'appelle personnellement le premier groupe de type méthode et le second groupe de type opérateur. Ce n'est pas une très bonne distinction, mais j'espère que cela aide d'une manière ou d'une autre.

Cela dit, lenne rentre pas exactement dans le deuxième groupe. C'est plus proche des opérations du premier, à la seule différence que c'est beaucoup plus courant que presque n'importe lequel d'entre eux. Mais la seule chose qu'il fait, c'est d'appeler __len__, et c'est très proche L.index. Cependant, il existe quelques différences. Par exemple, __len__peut être appelé pour l'implémentation d'autres fonctionnalités, par exemple bool, si la méthode a été appelée, lenvous pouvez rompre bool(x)avec une lenméthode personnalisée qui fait des choses complètement différentes.

En bref, vous avez un ensemble de fonctionnalités très courantes que les classes pourraient implémenter et qui pourraient être accessibles via un opérateur, via une fonction spéciale (qui fait généralement plus que l'implémentation, comme le ferait un opérateur), lors de la construction d'objet, et toutes partagent certains traits communs. Tout le reste est une méthode. Et lenc'est en quelque sorte une exception à cette règle.

Rosh Oxymoron
la source
0

Il n'y a pas grand chose à ajouter aux deux articles ci-dessus, mais toutes les fonctions "magiques" ne sont pas vraiment magiques du tout. Ils font partie du module __ builtins__ qui est implicitement / automatiquement importé au démarrage de l'interpréteur. C'est à dire:

from __builtins__ import *

se produit chaque fois avant le démarrage de votre programme.

J'ai toujours pensé que ce serait plus correct si Python ne faisait cela que pour le shell interactif et exigeait des scripts pour importer les différentes parties à partir des composants intégrés dont ils avaient besoin. Une gestion __ main__ probablement différente serait également intéressante dans les shells vs interactifs. Quoi qu'il en soit, consultez toutes les fonctions et voyez à quoi cela ressemble sans elles:

dir (__builtins__)
...
del __builtins__
user318904
la source