¿Cuál es la diferencia entre una función, un método independiente y un método vinculado?

Estoy haciendo esta pregunta debido a una discusión en el hilo de comentarios de esta respuesta . Soy el 90% del camino para conseguir mi cabeza alrededor de ella.

In [1]: class A(object): # class named 'A' ...: def f1(self): pass ...: In [2]: a = A() # an instance 

f1 existe en tres formas diferentes:

 In [3]: a.f1 # a bound method Out[3]: <bound method a.f1 of > In [4]: A.f1 # an unbound method Out[4]:  In [5]: a.__dict__['f1'] # doesn't exist KeyError: 'f1' In [6]: A.__dict__['f1'] # a function Out[6]:  

¿Cuál es la diferencia entre el método enlazado , el método no enlazado y los objetos de función , todos los cuales están descritos por f1? ¿Cómo se llama a estos tres objetos? ¿Cómo pueden ser transformados el uno en el otro? La documentación sobre este tema es bastante difícil de entender.

Una función es creada por la instrucción def , o por lambda . Bajo Python 2, cuando una función aparece dentro del cuerpo de una statement de class (o se pasa a una llamada de construcción de clase de type ), se transforma en un método independiente . (Python 3 no tiene métodos no vinculados ; consulte a continuación). Cuando se accede a una función en una instancia de clase, se transforma en un método enlazado , que proporciona automáticamente la instancia al método como primer parámetro self .

 def f1(self): pass 

Aquí f1 es una función .

 class C(object): f1 = f1 

Ahora C.f1 es un método C.f1 .

 >>> C.f1  >>> C.f1.im_func is f1 True 

También podemos usar el constructor de clase de type :

 >>> C2 = type('C2', (object,), {'f1': f1}) >>> C2.f1  

Podemos convertir f1 a un método no vinculado manualmente:

 >>> import types >>> types.MethodType(f1, None, C)  

Los métodos no vinculados están limitados por el acceso en una instancia de clase:

 >>> C().f1 > 

El acceso se traduce en llamadas a través del protocolo descriptor:

 >>> C.f1.__get__(C(), C) > 

Combinando estos:

 >>> types.MethodType(f1, None, C).__get__(C(), C) > 

O directamente:

 >>> types.MethodType(f1, C(), C) > 

La principal diferencia entre una función y un método independiente es que este último sabe a qué clase está vinculado; llamar o vincular un método independiente requiere una instancia de su tipo de clase:

 >>> f1(None) >>> C.f1(None) TypeError: unbound method f1() must be called with C instance as first argument (got NoneType instance instead) >>> class D(object): pass >>> f1.__get__(D(), D) > >>> C.f1.__get__(D(), D)  

Dado que la diferencia entre una función y un método independiente es bastante mínima, Python 3 se deshace de la distinción; en Python 3, acceder a una función en una instancia de clase solo te da la función en sí:

 >>> C.f1  >>> C.f1 is f1 True 

Tanto en Python 2 como en Python 3, estos tres son equivalentes:

 f1(C()) C.f1(C()) C().f1() 

La vinculación de una función a una instancia tiene el efecto de fijar su primer parámetro (convencionalmente llamado self ) a la instancia. Por lo tanto, el método enlazado C().f1 es equivalente a cualquiera de:

 (lamdba *args, **kwargs: f1(C(), *args, **kwargs)) functools.partial(f1, C()) 

es bastante dificil de entender

Bueno, es un tema bastante difícil, y tiene que ver con descriptores.

Vamos a empezar con la función. Todo está claro aquí: simplemente llámelo, todos los argumentos proporcionados se pasan mientras se ejecuta:

 >>> f = A.__dict__['f1'] >>> f(1) 1 

Regular TypeError se TypeError en caso de cualquier problema con el número de parámetros:

 >>> f() Traceback (most recent call last): File "", line 1, in  TypeError: f1() takes exactly 1 argument (0 given) 

Ahora, los métodos. Los métodos son funciones con un poco de especias. Los descriptores vienen en juego aquí. Como se describe en el Modelo de datos , A.f1 y A().f1 se convierten a A.__dict__['f1'].__get__(None, A) y type(a).__dict__['f1'].__get__(a, type(a)) respectivamente. Y los resultados de estos __get__ ‘s difieren de la función f1 sin f1 . Estos objetos son envolturas alrededor del f1 original y contienen alguna lógica adicional.

En el caso de un unbound method esta lógica incluye una verificación de si el primer argumento es una instancia de A :

 >>> f = A.f1 >>> f() Traceback (most recent call last): File "", line 1, in  TypeError: unbound method f1() must be called with A instance as first argument (got nothing instead) >>> f(1) Traceback (most recent call last): File "", line 1, in  TypeError: unbound method f1() must be called with A instance as first argument (got int instance instead) 

Si esta comprobación se realiza correctamente, ejecuta f1 original con esa instancia como primer argumento:

 >>> f(A()) <__main__.A object at 0x800f238d0> 

Tenga en cuenta que el atributo im_self es None :

 >>> f.im_self is None True 

En el caso del bound method esta lógica proporciona inmediatamente a f1 original una instancia de A que fue creada (esta instancia se almacena realmente en el atributo im_self ):

 >>> f = A().f1 >>> f.im_self <__main__.A object at 0x800f23950> >>> f() <__main__.A object at 0x800f23950> 

Entonces, bound significa que la función subyacente está vinculada a alguna instancia. unbound significa que todavía está vinculado, pero solo a una clase.

El objeto de función es un objeto llamable creado por una definición de función. Los métodos enlazados y no enlazados son objetos invocables creados por un Descriptor llamado por el operador binario de puntos.

Los objetos de método enlazados y no enlazados tienen 3 propiedades principales: im_func es el objeto de función definido en la clase, im_class es la clase e im_self es la instancia de la clase. Para los métodos no im_self , im_self es None .

Cuando se llama a un método enlazado, llama a im_func con im_self cuando el primer parámetro sigue sus parámetros de llamada. Los métodos no vinculados llaman a la función subyacente con solo sus parámetros de llamada.

Una cosa interesante que vi hoy es que, cuando asigno una función a un miembro de la clase, se convierte en un método independiente. Como:

 class Test(object): @classmethod def initialize_class(cls): def print_string(self, str): print(str) # Here if I do print(print_string), I see a function cls.print_proc = print_string # Here if I do print(cls.print_proc), I see an unbound method; so if I # get a Test object o, I can call o.print_proc("Hello") 

Consulte la documentación de Python 2 y Python 3 para obtener más detalles.

Mi interpretación es la siguiente.

Fragmentos de Function clase:

Python 3:

 class Function(object): . . . def __get__(self, obj, objtype=None): "Simulate func_descr_get() in Objects/funcobject.c" if obj is None: return self return types.MethodType(self, obj) 

Python 2:

 class Function(object): . . . def __get__(self, obj, objtype=None): "Simulate func_descr_get() in Objects/funcobject.c" return types.MethodType(self, obj, objtype) 
  1. Si se llama a una función sin clase o instancia, es una función simple.
  2. Si se llama a una función desde una clase o una instancia, se llama a su __get__ para recuperar la función envuelta:
    a. Bx es lo mismo que B.__dict__['x'].__get__(None, B) . En Python 3, esto devuelve una función simple. En Python 2, esto devuelve una función independiente.

    segundo. bx es igual que type(b).__dict__['x'].__get__(b, type(b) . Esto devolverá un método enlazado tanto en Python 2 como en Python 3, lo que significa que self pasará implícitamente como primer argumento.