8

Tengo una duda con los métodos en python. Según la documentación de Python sobre el atributo especial __dict__:

A dictionary or other mapping object used to store an object’s (writable) attributes.

Traducción:

Un diccionario u otro objeto de mapeo utilizado para almacenar los atributos (de escritura) de un objeto.

En pocas palabras el atributo __dict__ almacena los atributos de un objeto.

Al momento de crear por ejemplo la siguiente clase:

>>> class A:
...    a = 1
...
...    def method(self):
...        print('method')
...
...
>>> A.__dict__
{'__module__': '__main__', 'a': 1, 'method': <function A.method at 0x7f4b2e802950>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}

Se puede ver que el "metodo" method, esta en el __dict__ de la clase, al igual que el atributo a.

Según la documentación de python, __dict__ almacena los atributos de un objeto (cabe recalcar que una clase es un objeto). ¿Entonces un método en realidad es un atributo?.

¿Se podría decir que method es un atributo que almacena una función?. Pero en ningún momento hice algo así:

def funcion(self):
    print(self.a)

class A:
    a = 1
    method = funcion

print(A().method())

Ademas de que funciona y la función function recibe como primer parámetro (self), la instancia, imprimiendo 1 como resultado.

Aquí me surge otra duda, el atributo method que defino en la clase A (method = funcion), es un método?


¿Entonses que exactamente es un método en python?

Seria de gran ayuda que me aclararan esta duda que tengo, gracias por sus respuestas.

Jbeltran
  • 766
  • 4
  • 15
  • Te respondo con dos preguntas: ¿Puedes definir una función sin darle un nombre, en una Clase en Python? Si, se diera el caso que pudieras hacerlo, ¿cómo la ejecutarías? Piensa un poco en eso y tal vez te ayude a entender lo que es un atributo, y el porqué un método de una Clase es un atributo de la misma. Saludos – Mauricio Contreras May 01 '20 at 07:42

1 Answers1

6

¿Entonces un método en realidad es un atributo?

La respuesta corta es si, para el nivel en el que la pregunta está enfocada a efectos prácticos lo es. Técnicamente cualquier cosa que se pueda referenciar a través de un objeto usando la notación .nombre (dotted expression) es considerado un atributo, eso incluye a los métodos.

IMPORTANTE

Ésto puede parecer que contradice los conceptos de atributo y método que se tienen en el paradigma de la POO, en la que a grandes rasgos un atributo es un una característica que describe a determinado objeto mientras que un método es algo que ese objeto puede hacer. Ésta separación se mantiene de forma conceptual a alto nivel en Python, conceptualmente un método y un atributo no son por supuesto lo mismo, pero en lo que al lenguaje concierne y a cómo se implementa la POO en Python la separación no es tan clara, más bien todo lo contrario, y de eso trata la pregunta y es a lo que se intenta responder en ésta respuesta.


¿El atributo method que defino en la clase A (method = funcion), es un método?

Si, lo es. Aunque el concepto de método normalmente se restringe a "función definida en el cuerpo de una clase", a efectos prácticos y no conceptuales, es un método. Se pueden definir los métodos fuera de la clase, incluso en otro módulo y luego enlazarlos así. O incluso después de la definición de la clase:

class Test:
    def foo(self):
        print(f"Hola, soy un método de {self} definido de forma 'normal'")
    
>>> inst = Test()
>>> inst.foo
<bound method Test.foo of <__main__.Test object at 0x7f13658fac70>>
>>> inst.foo()
Hola, soy un método de <__main__.Test object at 0x7f13658fac70> definido de forma 'normal'

>>> inst.bar()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute 'bar'

>>> def bar(self):
        print(f"Soy un método de {self} definido a 'distancia'")

>>> Test.bar = bar
>>> inst.bar
<bound method bar of <__main__.Test object at 0x7f13658fac70>>
>>> inst.bar()
Soy un método de <__main__.Test object at 0x7f13658fac70> definido a 'distancia'

¡Toma! Lenguaje dinámico en todo su esplendor, para lo bueno y para lo malo... :)

Que se pueda hacer no significa que sea buena idea, Python deja hacer casi todo, muy buenas ideas y muy malas ideas. Cuando se suele restringir algo es porque pone en peligro al propio intérprete o por una muy buena razón de diseño, optimización, etc. En vez de hacer ésto tenemos la herencia por ejemplo, pero si podemos incluso agregar métodos a una clase con instancias creadas y encima llamarlos desde la instancias ya creadas sin ningún problema.


¿Entonces qué es exactamente un método en Python?

Si alguien tiene gana de leer un ratito con posibilidad de no entender nada al final por mi culpa queda avisado, pero ahí va.

En Python prácticamente todo es un objeto en memoria (incluidos los módulos importados, el propio script, funciones, clases, enteros, etc), que a su vez tienen métodos, funciones o cualquier cosa con un método __call __(), como objetos que pueden ser llamados.

Al sistema de resolución de atributos (en el que __dict__ tiene una función primordial) no le importa en absoluto que el objeto sea callable o no. Solo trata de buscar ese nombre en la clase o las clases de las que deriva siguiendo el MRO. Luego tu lo intentas llamar, le intentas asignar o le pegas fuego, pero su misión termina cuanto te da una referencia al atributo o una excepción porque no lo ha podido encontrar.

En realidad los métodos son objetos que actúan de envoltorios para funciones manteniendo referencias a la instancia a la que pertenecen y enlazando así la función con su instancia. Además son creados sobre la marcha al acceder a la función como un atributo mediante la mencionada sintaxis .nombre.

Si nos fijamos en como se define un método, no difiere en nada de definir una función, lo único que cambia es que se recibe una referencia a clase o a la instancia como primer argumento (cls/self por convención nada más).

Obviamente tiene que haber un mecanismo que una la función con la clase o instancia y que permita referenciar al método con .nombre. Python resuelve ésto usando descriptores de no datos. Al final lo veremos por encima.

Hay que tener en cuenta que Python es un lenguaje dinámico, en la que una variable/atributo no deja de ser solo un nombre asociado en todo momento a una referencia a un objeto en memoria.

Esto quiere decir que en el fondo todas las variables/atributos son lo mismo, todas son solo caminos para encontrar un objeto en memoria, no importa que el objeto sea callable o no, o que en un momento dado la variable apunte a otro objeto o que varias apunten al mismo. El tipo y las propiedades pertenecen siempre al objeto, no a la variable. Realmente para el lenguaje no importa si instancia.nombre es un entero, una cadena , una lista o una método, todo son objetos.

En los lenguajes como C++, ésto no pasa, los datos y los métodos están claramente separados, con los datos (atributos) fijados a un tipo en concreto y en el que los métodos no son objetos.

El atributo __dict__

El atributo __dict__ de un objeto (si también es un atributo) se comporta de forma parecida a un diccionario, pero no lo es. Realmente es una instancia de una clase DictProxy cuyos objetos se comportan como diccionarios pero con algunas diferencias, por ejemplo, no podemos agregar atributos cómo hacemos con las claves de un diccionario, ésto no funciona:

 Clase.__dict__["foo"] = 13
 
 

debemos usar:

Clase.foo = 13

o

setattr(Clase, "foo", 13)

Por ejemplo, si somos tan inconscientes de hacer ésto:

del instancia.__dict__

se crea de nuevo...

Las razones de usar DictProxy son varias, en esencia se hace por optimización, forzando a que las claves siempre sean cadenas, lo cual permite que intérprete pueda realizar ciertas optimizaciones. También por seguridad, para evitar que cosas como la de arriba mutilen una clase o instancia y puedan mandar al intérprete mismo a tomar viento...

los "diccionarios" de clases como __dict__ simplemente almacenan métodos como funciones.

Los descriptores

Un descriptor es un objeto que tiene al menos uno de los siguientes métodos mágicos en sus atributos: __get__, __set__ o __delete__. Su implementación y uso se conoce como protocolo descriptor y que básicamente es cambiar un atributo por un objeto (el descriptor) que intermedia en el acceso a ese atributo, de ésta forma permiten definir y establecer el comportamiento del atributo de un objeto.

Un descriptor que solo implementa __get__ se llama descriptor de no datos y son usados para acceder a los métodos como veremos. Mientras que si implementa además __set__ son descriptores de datos, que no nos interesan mucho para éste caso.

Los descriptores se utilizan para muchas cosas relacionadas con los atributos y los métodos, están por todos lados en Python aunque no los veamos. Por ejemplo para los métodos estáticos (@staticmethod), los métodos de clase (@classmethod) y las propiedades (@property) se usan descriptores, los decoradores no son nada más que una forma de implementarlos de manera simple sin que parezca que se están usando.

Las propiedades son quizás un ejemplo muy claro de ésto:

class Foo:
    def __init__(self):
        self._bar = None
        
    @property
    def bar(self):
        return self._bar
        
    @bar.setter
    def bar(self, valor):
        self._bar = valor
        
>>> inst = Foo()  
>>> inst.bar = 5  
>>> inst.bar  
5

Pero todo ésto va mucho más lejos, si no no me hubiera metido en el "charco" de los descriptores. Como mencionaba antes, para permitir que una función sea llamada como métodos necesitan algo más. Las funciones incluyen el método __get__() para vincular la función con el acceso a tributos mediante .nombre, por lo tanto todas las funciones son descriptores de no datos que devuelven métodos vinculados cuando se invocan desde un objeto.

De hecho, si accedemos a un método a través de __dict__, no se llama a __get__, sino que se retorna una referencia a la función que hay debajo sin más:

class Foo:
    def bar(self):
        pass
>>> Foo.__dict__["bar"]
<function Foo.bar at 0x7f0345da3a60>

Si accedemos usando Clase.método, si se invoca __get__, dado que como se ha comentado es el enlace entre la función y la clase para permitir dicha sintaxis, pero se retorna también el objeto función subyacente sin más:

>>> Foo.bar
<function Foo.bar at 0x7f0345da3a60>

En cambio, cuando se accede mediante la instancia, la función se envuelve sobre la marcha en un método enlazado:

>>> Foo().bar
<bound method Foo.bar of <__main__.Foo object at 0x7f0345df0460>>

Dicho objeto almacena internamente la referencia a la función y a la instancia a la que está asociada, entre otras cosas:

>>> Foo().bar.__func__
<function Foo.bar at 0x7f0345da3a60>
>>> Foo().bar.__self__
<__main__.Foo object at 0x7f0345df0460>

Un método, por tanto, no es más que un atributo enlazado a una función a través de un descriptor.

FJSevilla
  • 55,603
  • 7
  • 35
  • 58
  • 1
    Muchas gracias por tu respuesta! estuvo excelente, pero en pocas palabras un método según Python, es un atributo verdad?, Por otra parte en el segundo ejemplo de mi pregunta, donde creo el atributo `method` dándole una función, se le considera un método? (ya que recibe como primer parámetro la instancia). – Alberja Verde May 01 '20 at 16:55
  • 2
    He editado, la respuesta, se me pasó comentar tu segunda duda. En esencia, si, es un método, en el momento que creas el atributo y le asignas una referencia a la función su comportamiento no difiere en nada de un método definido dentro de la clase. – FJSevilla May 01 '20 at 18:15
  • ¿No crees que decir que un método es un atributo puede causar confusión en Python? Si nos enfocamos en el idioma (o sea, Python), desde mi humilde opinión, no considero que un método sea un atributo, porque son dos conceptos diferentes que se manejan en este lenguaje de alto nivel. Pero si hablamos a nivel de implementación (de como funciona a bajo nivel), ahí sería diferente, puesto que un objeto es simplemente una región de memoria, por lo tanto, un atributo, método, función, son objetos, porque internamente almacenan información. – MrDave1999 May 01 '20 at 19:03
  • La pregunta del OP desde mi perspectiva esta mas enfocada a como realmente funciona las cosas, no a Python. – MrDave1999 May 01 '20 at 19:03
  • 2
    @MrDave1999, mi pregunta estaba enfocada a como funcionan a profundidad los métodos y la POO en general, pero en **Python específicamente**, con el fin de entender como funciona Python realmente, y esta respuesta responde a mis dudas. – Alberja Verde May 01 '20 at 19:10
  • 1
    @FJSevilla tu respuesta estuvo realmente excelente!! Muchas gracias por tomarte el tiempo de formular esta respuesta tan buena! Eres un el puto amo! xd – Alberja Verde May 01 '20 at 19:12
  • @AlberjaVerde En Python, un método no es un atributo, pero si nos enfocamos a como funciona internamente, si es lo mismo, porque todos esos conceptos de alto nivel son lo mismo si lo analizas a bajo nivel. Pero no debes mezclar los conceptos del lenguaje con su implementación, eso causa a cualquier una confusión. – MrDave1999 May 01 '20 at 19:13
  • 2
    @MrDave1999 No los estoy mezclando, solo quería entender como funciona a profundidad realmente Python! – Alberja Verde May 01 '20 at 19:18
  • 2
    @MrDave1999, creo que la pregunta deja bastante claro que se busca entender cómo Python implementa la POO y por qué la propia documentación a pesar de que `__dict__` contiene referencias a métodos y a atributos los engloba todo como atributos. En mi respuesta en ningún momento digo que a nivel abstracto y conceptual sean los mismo, por supuesto que no, igual que un método de clase no es lo mismo que uno de instancia o uno estático. – FJSevilla May 01 '20 at 20:17
  • 4
    Pero a nivel de implementación interna la diferencia entre método y atributo no es tal y la pregunta va por ahí. Sea como sea, por si alguien se queda solo con la primera parte de la respuesta, he movido la parte dónde lo mencionaba al principio de la misma y lo he intentado explicitar más para ver si queda más claro que no estamos hablando a niveles abstractos y conceptuales, sino de implementación concreta en Python para evitar las posibles confusiones, en lo que si puedes tener razón. Un saludo. – FJSevilla May 01 '20 at 20:18