¿Por qué Python usa 'métodos mágicos'?

99

He estado jugando con Python recientemente, y una cosa que encuentro un poco extraña es el uso extensivo de 'métodos mágicos', por ejemplo, para hacer que su longitud esté disponible, un objeto implementa un método def __len__(self), y luego se llama cuando escribe len(obj).

Me preguntaba por qué los objetos no definen simplemente un len(self)método y lo llaman directamente como miembro del objeto, por ejemplo obj.len(). Estoy seguro de que debe haber buenas razones para que Python lo haga de la forma en que lo hace, pero como novato aún no he descubierto cuáles son.

Greg Beech
fuente
4
Creo que la razón general es a) histórica yb) algo similar len()o se reversed()aplica a muchos tipos de objetos, pero un método como append()solo se aplica a secuencias, etc.
Grant Paul

Respuestas:

64

AFAIK, lenes especial en este sentido y tiene raíces históricas.

Aquí hay una cita de las preguntas frecuentes :

¿Por qué Python usa métodos para algunas funciones (por ejemplo, list.index ()) pero funciones para otras (por ejemplo, len (lista))?

La principal razón es la historia. Las funciones se utilizaron para aquellas operaciones que eran genéricas para un grupo de tipos y que estaban destinadas a funcionar incluso para objetos que no tenían ningún método (por ejemplo, tuplas). También es conveniente tener una función que se pueda aplicar fácilmente a una colección amorfa de objetos cuando usa las características funcionales de Python (map (), apply () et al).

De hecho, implementar len (), max (), min () como una función incorporada es en realidad menos código que implementarlos como métodos para cada tipo. Se pueden objetar casos individuales, pero es parte de Python, y es demasiado tarde para hacer cambios tan fundamentales ahora. Las funciones deben permanecer para evitar una rotura masiva del código.

Los otros "métodos mágicos" (en realidad llamados métodos especiales en el folclore de Python) tienen mucho sentido y existen funciones similares en otros lenguajes. Se usan principalmente para código que se llama implícitamente cuando se usa una sintaxis especial.

Por ejemplo:

  • operadores sobrecargados (existen en C ++ y otros)
  • constructor / destructor
  • ganchos para acceder a los atributos
  • herramientas para la metaprogramación

y así...

Eli Bendersky
fuente
2
Python and the Principle of Least Astonishment es una buena lectura para algunas de las ventajas de que Python sea de esta manera (aunque admito que el inglés necesita mejorar). El punto básico: permite que la biblioteca estándar implemente una tonelada de código que se vuelve muy, muy reutilizable pero aún anulable.
jpmc26
20

Del Zen de Python:

Ante la ambigüedad, rechace la tentación de adivinar.
Debe haber una, y preferiblemente solo una, forma obvia de hacerlo.

Esta es una de las razones - con métodos personalizados, los desarrolladores tendrán la libertad de elegir un nombre de método diferente, como getLength(), length(), getlength()o en absoluto. Python impone un nombre estricto para que len()se pueda usar la función común .

Todas las operaciones que son comunes para muchos tipos de objetos se ponen en métodos mágicos, como __nonzero__, __len__o __repr__. Sin embargo, en su mayoría son opcionales.

La sobrecarga de operadores también se realiza con métodos mágicos (p __le__. Ej. ), Por lo que tiene sentido usarlos también para otras operaciones comunes.

AndiDog
fuente
Este es un argumento convincente. Más satisfactorio que "Guido realmente no creía en OO" .... (como he visto en otra parte).
Andy Hayden
15

Python usa la palabra "métodos mágicos" , porque esos métodos realmente hacen magia para su programa. Una de las mayores ventajas de utilizar los métodos mágicos de Python es que proporcionan una forma sencilla de hacer que los objetos se comporten como tipos integrados. Eso significa que puede evitar formas desagradables, contrarias a la intuición y no estándar de realizar operadores básicos.

Considere el siguiente ejemplo:

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'

Esto da un error, porque el tipo de diccionario no admite la adición. Ahora, ampliemos la clase de diccionario y agreguemos el método mágico "__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)

Ahora, da la siguiente salida.

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

Por lo tanto, al agregar este método, de repente ha sucedido la magia y el error que estaba recibiendo antes ha desaparecido.

Espero que te aclare las cosas. Para obtener más información, consulte:

Una guía de los métodos mágicos de Python (Rafe Kettler, 2012)

Mangu Singh Rajpurohit
fuente
9

Algunas de estas funciones hacen más de lo que un solo método podría implementar (sin métodos abstractos en una superclase). Por ejemplo, bool()actúa así:

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

También puede estar 100% seguro de que bool()siempre devolverá Verdadero o Falso; si confiara en un método, no podría estar completamente seguro de lo que obtendría.

Algunas otras funciones que tienen implementaciones relativamente complicadas (más complicadas de lo que probablemente sean los métodos mágicos subyacentes) son iter()y cmp(), y todos los métodos de atributo ( getattr, setattry delattr). Cosas como inttambién acceden a métodos mágicos al hacer coerción (puede implementar __int__), pero cumplen una doble función como tipos. len(obj)es en realidad el único caso en el que no creo que sea diferente obj.__len__().

Ian Bicking
fuente
2
En lugar de hasattr()usaría try:/ except AttributeError:y en lugar de if obj.__len__(): return True else: return Falsesolo diría, return obj.__len__() > 0pero esas son solo cosas estilísticas.
Chris Lutz
En Python 2.6 (al que se bool(x)hace referencia por cierto x.__nonzero__()), su método no funcionaría. Las instancias bool tienen un método __nonzero__(), y su código seguiría llamándose a sí mismo una vez que obj fuera bool. ¿Quizás bool(obj.__bool__())debería ser tratado de la misma manera que usted __len__? (¿O este código realmente funciona para Python 3?)
Ponkadoodle
La naturaleza circular de bool () era algo intencionalmente absurda, para reflejar la naturaleza peculiarmente circular de la definición. Existe un argumento de que simplemente debería considerarse un primitivo.
Ian Bicking
La única diferencia (actualmente) entre len(x)y x.__len__()es que el primero generará OverflowError para longitudes que excedan sys.maxsize, mientras que el segundo generalmente no lo hará para tipos implementados en Python. Sin embargo, eso es más un error que una característica (por ejemplo, el objeto de rango de Python 3.2 puede manejar rangos arbitrariamente grandes, pero usarlos lencon ellos puede fallar. Sin __len__embargo, también fallan, ya que están implementados en C en lugar de Python)
ncoghlan
4

En realidad, no son "nombres mágicos". Es solo la interfaz que un objeto tiene que implementar para proporcionar un servicio determinado. En este sentido, no son más mágicas que cualquier definición de interfaz predefinida que tengas que volver a implementar.

Stefano Borini
fuente
1

Si bien la razón es principalmente histórica, existen algunas peculiaridades en la len que hacen que el uso de una función en lugar de un método sea apropiado.

Algunas operaciones en Python se implementan como métodos, por ejemplo list.indexy dict.append, mientras que otras se implementan como métodos invocables y mágicos, por ejemplo stry itery reversed. Los dos grupos difieren lo suficiente como para justificar el enfoque diferente:

  1. Son comunes.
  2. str, int Y los amigos son tipos. Tiene más sentido llamar al constructor.
  3. La implementación difiere de la llamada a la función. Por ejemplo, iterpodría llamar __getitem__si __iter__no está disponible y admite argumentos adicionales que no encajan en una llamada de método. Por la misma razón it.next()se ha cambiado anext(it) en versiones recientes de Python: tiene más sentido.
  4. Algunos de ellos son parientes cercanos de los operadores. Hay sintaxis para llamar __iter__y __next__se llama forbucle. Por coherencia, una función es mejor. Y lo hace mejor para ciertas optimizaciones.
  5. Algunas de las funciones son simplemente demasiado similares al resto de alguna manera: repractúa como lo strhace. Tener str(x)versusx.repr() sería confuso.
  6. Algunos de ellos rara vez utilizan el método de implementación real, por ejemplo isinstance .
  7. Algunos de ellos son operadores reales, getattr(x, 'a')es otra forma de hacer x.ay getattrcomparte muchas de las cualidades antes mencionadas.

Yo personalmente llamo al primer grupo como método y al segundo grupo como operador. No es una distinción muy buena, pero espero que ayude de alguna manera.

Dicho esto, lenno encaja exactamente en el segundo grupo. Está más cerca de las operaciones en la primera, con la única diferencia de que es mucho más común que casi todas. Pero lo único que hace es llamar __len__, y está muy cerca L.index. Sin embargo, hay algunas diferencias. Por ejemplo, __len__podría ser llamado para la implementación de otras características, como por ejemplo bool, si se llamó al método len, podría romper bool(x)con un lenmétodo personalizado que hace algo completamente diferente.

En resumen, tiene un conjunto de características muy comunes que las clases pueden implementar a las que se puede acceder a través de un operador, a través de una función especial (que generalmente hace más que la implementación, como lo haría un operador), durante la construcción del objeto, y todas ellas. comparten algunos rasgos comunes. Todo lo demás es un método. Y lenes una excepción a esa regla.

Rosh Oxymoron
fuente
0

No hay mucho que agregar a las dos publicaciones anteriores, pero todas las funciones "mágicas" no son realmente mágicas en absoluto. Son parte del módulo __ builtins__ que se importa implícita / automáticamente cuando se inicia el intérprete. Es decir:

from __builtins__ import *

sucede cada vez antes de que comience su programa.

Siempre pensé que sería más correcto si Python solo hiciera esto para el shell interactivo, y requiriera scripts para importar las diversas partes de las incorporaciones que necesitaban. También probablemente un manejo __ main__ diferente sería bueno en shells frente a interactivo. De todos modos, revise todas las funciones y vea cómo es sin ellas:

dir (__builtins__)
...
del __builtins__
user318904
fuente