Pregunta Objeto de función llamado por atributo de clase falla


Actualmente tengo un sistema de clases que registra devoluciones de llamada y luego las llamo cuando se cumplen ciertas condiciones. Sin embargo, estoy teniendo problemas para almacenar el objeto de función.

UN:

class Foo(object):
  _callback = None

  @classmethod
  def Register(cls, fn):
    cls._callback = fn

def Bar():
  print "Called"

Foo.Register(Bar)
Foo._callback()

input clear Python 2.7.10 (default, Jul 14 2015, 19:46:27) [GCC 4.8.2] on linux

Traceback (most recent call last):   File "python", line 12, in <module> 
TypeError: unbound method Bar() must be called with Foo instance as first argument (got nothing instead)

No estoy seguro de por qué requiere una instancia de Foo cuando la función no es miembro de Foo.

SEGUNDO:

class Foo(object):
  _callback = []

  @classmethod
  def Register(cls, fn):
    cls._callback.append(fn)

def Bar():
  print "Called"

Foo.Register(Bar)
Foo._callback[0]()

¿Por qué la primera versión no funciona mientras que la segunda versión? Qué funcionalidad difiere al agregarlo a una lista.


7
2018-06-08 18:42


origen


Respuestas:


Cada vez que asigna una función a un objeto de clase, se convierte en un método:

In [6]: Foo.baz = lambda self: 'baz'

In [7]: f = Foo()

In [8]: f.baz()
Out[8]: 'baz'

Tenga en cuenta que esto significa que puede agregar dinámicamente métodos a clases, que serán visibles en objetos instanciados.

In [9]: Foo.anothermethod = lambda self, x: x*2

In [10]: f.anothermethod(4)
Out[10]: 8

Desde el documentos:

Si aún no comprende cómo funcionan los métodos, observe el   la implementación tal vez pueda aclarar las cosas. Cuando un atributo de instancia   se hace referencia a que no es un atributo de datos, se busca su clase. Si   el nombre denota un atributo de clase válido que es un objeto de función, un   El objeto método se crea empaquetando (punteros) el objeto instancia   y el objeto de función que acaba de encontrarse en un objeto abstracto:   este es el objeto de método. Cuando el objeto método se llama con un   lista de argumentos, una nueva lista de argumentos se construye a partir de la instancia   objeto y la lista de argumentos, y se llama al objeto de función con   esta nueva lista de argumentos


4
2018-06-08 19:00



Cuando asignes Bar a cls._callback, _callback se convierte automáticamente en un método independiente de Foo (ya que es una función).

Por lo tanto, cuando se invoca, se espera que la instancia sea su primer argumento, lo que da como resultado un error en los casos en que no se pasa nada (espera una instancia) o pasa una instancia (Bar admite 0 argumentos).

Sin embargo, si anexas Bar a una lista en su lugar, y luego acceder al elemento de la lista, tiene el normal-viejo-bueno Bar listo para el uso sin ningún tipo de convenciones limitantes que lo perjudiquen, porque simplemente se conserva como un objeto, no como un método vinculado.


4
2018-06-08 19:01



. Asignarlo es simplemente agregarlo a las clases __dict__ que almacena las referencias de los métodos vinculados y no vinculados (es decir, funciones). Entonces, al hacer esto, simplemente agrega otra referencia a una función independiente que espera una instancia como primer argumento.

Aquí hay un ejemplo más fácil:

class Foo:
    test = lambda x: x

Foo.test("Hello world!")

Error:

unbound method <lambda>(): must be called....

3
2018-06-08 19:00



Aún tienes tu objeto de función. Sin embargo, llamar a la función desde la clase la transforma en un método independiente cada vez que se accede a través de la clase:

Tenga en cuenta que la transformación de objeto de función a (sin consolidar o   obligado) el objeto de método ocurre cada vez que se recupera el atributo de   la clase o instancia.

(Lo anterior se encuentra debajo métodos definidos por el usuario del Documentación del modelo de datos de Python 2)

Por lo tanto, se requiere que la función que ahora se invoca a través de una clase se pase una instancia.

Tenga en cuenta que los métodos independientes solo existen en Python 2, por lo que su código es válido y funcionará en Python 3.

Puede recuperar el objeto de función del método independiente al acceder a su im_func atributo:

Foo._callback.im_func()
# Called

El segundo caso almacena su objeto de función en un contenedor, no tiene ningún negocio directo con la clase y se puede recuperar mediante, por ejemplo, indexación y llamada normalmente.


3
2018-06-08 19:15



los documentación en realidad proporciona un ejemplo muy similar al tuyo. En la descripción de "Objetos de método":

Cuando se hace referencia a un atributo de instancia que no es un atributo de datos, se busca su clase. Si el nombre denota un atributo de clase válido que es un objeto de función, se crea un objeto de método empacando (punteros) el objeto de instancia y el objeto de función que se encuentran juntos en un objeto abstracto: este es el objeto de método.

y de "Comentarios aleatorios":

No es necesario que la definición de la función esté textualmente encerrada en la definición de la clase: la asignación de un objeto de función a una variable local en la clase también está bien.

La última cita es seguida por el ejemplo al que me refiero.

Básicamente, la idea es que cualquier función nombrada en una definición de clase se convierta en un método de esa clase. Como no está creando una instancia de su clase, su método nunca se vincula a un objeto de instancia (por lo tanto, ... unbound method Bar() ...) Los métodos siempre se llaman con un primer parámetro que contiene el objeto en el que se invocan, te guste o no.

Esta es la razón por la que debe tener un decorador especial para @classmethod por cierto. El decorador ajusta la función que creó para darle la interfaz requerida, pero pasa el objeto de clase en lugar del objeto de instancia a la función envuelta que usted definió.

Desde que tu segunda versión crea _callback como objeto de datos, simplemente almacena un puntero de función normal en una lista y lo llama tal como está.

Si intentaras pasar algo como None a Foo._callback(None), se librará del error original que está recibiendo, pero por supuesto se encontrará con el problema de pasar un parámetro que la función no esperaba. Puede solucionar este problema dando Bar un argumento (que ignorarías en este caso):

def Bar(self):
    print "Called"

2
2018-06-08 19:10