¿Cuáles son las diferencias entre un método de clase y un método de metaclase?


12

En Python, puedo crear un método de clase usando el @classmethoddecorador:

>>> class C:
...     @classmethod
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> C.f()
f called with cls=<class '__main__.C'>

Alternativamente, puedo usar un método normal (instancia) en una metaclase:

>>> class M(type):
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> class C(metaclass=M):
...     pass
...
>>> C.f()
f called with cls=<class '__main__.C'>

Como se muestra en la salida de C.f(), estos dos enfoques proporcionan una funcionalidad similar.

¿Cuáles son las diferencias entre usar @classmethody usar un método normal en una metaclase?

Respuestas:


5

Como las clases son instancias de una metaclase, no es inesperado que un "método de instancia" en la metaclase se comporte como un método de clase.

Sin embargo, sí, hay diferencias, y algunas de ellas son más que semánticas:

  1. La diferencia más importante es que un método en la metaclase no es "visible" desde una instancia de clase . Esto sucede porque la búsqueda de atributos en Python (de manera simplificada, los descriptores pueden tener prioridad) busca un atributo en la instancia; si no está presente en la instancia, Python busca en la clase de esa instancia y luego la búsqueda continúa las superclases de la clase, pero no en las clases de la clase. Python stdlib hace uso de esta característica en el abc.ABCMeta.registermétodo. Esa característica se puede usar para bien, ya que los métodos relacionados con la clase en sí mismos se pueden reutilizar como atributos de instancia sin ningún conflicto (pero un método seguiría en conflicto).
  2. Otra diferencia, aunque obvia, es que un método declarado en la metaclase puede estar disponible en varias clases, no relacionadas de otra manera: si tiene diferentes jerarquías de clases, no están relacionadas en absoluto con lo que tratan, pero desea alguna funcionalidad común para todas las clases , tendría que crear una clase mixin, que debería incluirse como base en ambas jerarquías (por ejemplo, para incluir todas las clases en un registro de aplicación). (Nota: el mixin a veces puede ser una mejor opción que una metaclase)
  3. Un método de clase es un objeto especializado de "método de clase", mientras que un método en la metaclase es una función ordinaria.

Entonces, sucede que el mecanismo que utilizan los métodos de clase es el " protocolo descriptor ". Mientras que las funciones normales presentan un __get__método que insertará el selfargumento cuando se recuperan de una instancia, y dejará ese argumento vacío cuando se recupere de una clase, un classmethodobjeto tiene un diferente __get__, que insertará la clase en sí (el "propietario") como primer parámetro en ambas situaciones.

Esto no hace diferencias prácticas la mayor parte del tiempo, pero si desea acceder al método como una función, con el fin de agregarle decorador dinámicamente, o cualquier otro, para un método en la metaclase meta.methodrecupera la función, lista para ser utilizada , mientras que tiene que usar cls.my_classmethod.__func__ para recuperarlo de un método de clase (y luego tiene que crear otro classmethodobjeto y asignarlo de nuevo, si realiza un ajuste ).

Básicamente, estos son los 2 ejemplos:


class M1(type):
    def clsmethod1(cls):
        pass

class CLS1(metaclass=M1):
    pass

def runtime_wrap(cls, method_name, wrapper):
    mcls = type(cls)
    setattr(mcls, method_name,  wrapper(getatttr(mcls, method_name)))

def wrapper(classmethod):
    def new_method(cls):
        print("wrapper called")
        return classmethod(cls)
    return new_method

runtime_wrap(cls1, "clsmethod1", wrapper)

class CLS2:
    @classmethod
    def classmethod2(cls):
        pass

 def runtime_wrap2(cls, method_name, wrapper):
    setattr(cls, method_name,  classmethod(
                wrapper(getatttr(cls, method_name).__func__)
        )
    )

runtime_wrap2(cls1, "clsmethod1", wrapper)

En otras palabras: aparte de la importante diferencia de que un método definido en la metaclase es visible desde la instancia y un classmethodobjeto no, las otras diferencias en tiempo de ejecución parecerán oscuras y sin sentido, pero eso sucede porque el lenguaje no necesita ir fuera de su camino con reglas especiales para métodos de clase: Ambas formas de declarar un método de clase son posibles, como consecuencia del diseño del lenguaje, una, por el hecho de que una clase es en sí misma un objeto, y otra, como una posibilidad entre muchos, de El uso del protocolo descriptor que permite especializar el acceso a atributos en una instancia y en una clase:

La classmethodconstrucción se define en código nativo, pero podría codificarse en Python puro y funcionaría exactamente de la misma manera. La clase de 5 líneas de abajo se puede usar como classmethoddecorador sin diferencias de tiempo de ejecución con la representación de @classmethod" at all (though distinguishable through introspection such as calls toinstancia , and evenintegrada, por supuesto):


class myclassmethod:
    def __init__(self, func):
        self.__func__ = func
    def __get__(self, instance, owner):
        return lambda *args, **kw: self.__func__(owner, *args, **kw)

Y, más allá de los métodos, es interesante tener en cuenta que los atributos especializados, como un @propertyen la metaclase, funcionarán como atributos de clase especializados, igual, sin ningún comportamiento sorprendente.


2

Cuando lo expresa como lo hizo en la pregunta, las @classmethodmetaclases y pueden parecer similares, pero tienen propósitos bastante diferentes. La clase que se inyecta en el @classmethodargumento de 's generalmente se usa para construir una instancia (es decir, un constructor alternativo). Por otro lado, las metaclases generalmente se usan para modificar la clase en sí (por ejemplo, como lo que hace Django con sus modelos DSL).

Eso no quiere decir que no pueda modificar la clase dentro de un método de clase. Pero entonces la pregunta es: ¿por qué no definiste la clase de la manera que deseas modificarla en primer lugar? De lo contrario, podría sugerir un refactor para usar varias clases.

Expandamos un poco el primer ejemplo.

class C:
    @classmethod
    def f(cls):
        print(f'f called with cls={cls}')

Tomando prestado de los documentos de Python , lo anterior se expandirá a algo como lo siguiente:

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

class C:
    def f(cls):
        print(f'f called with cls={cls}')
    f = ClassMethod(f)

Nota cómo __get__se puede tomar ya sea una instancia o la clase (o ambos), y por lo tanto se puede hacer ambas cosas C.fy C().f. Esto es diferente al ejemplo de metaclase que das que arrojará un AttributeErrorfor C().f.

Además, en el ejemplo de metaclase, fno existe en C.__dict__. Al buscar el atributo fcon C.f, el intérprete mira C.__dict__y luego, después de no encontrarlo, mira type(C).__dict__(que es M.__dict__). Esto puede importar si desea que la flexibilidad para anular fen C, aunque dudo que esto volverá a ser de uso práctico.


0

En su ejemplo, la diferencia estaría en algunas otras clases que tendrán M establecida como su metaclase.

class M(type):
    def f(cls):
        pass

class C(metaclass=M):
    pass

class C2(metaclass=M):
    pass

C.f()
C2.f()
class M(type):
     pass

class C(metaclass=M):
     @classmethod
     def f(cls):
        pass

class C2(metaclass=M):
    pass

C.f()
# C2 does not have 'f'

Aquí hay más sobre metaclases ¿Cuáles son algunos casos de uso (concretos) para metaclases?


0

Tanto @classmethod como Metaclass son diferentes.

Todo en Python es un objeto. Todo significa todo.

¿Qué es la metaclase?

Como se ha dicho, todo es un objeto. Las clases también son objetos, de hecho, las clases son instancias de otros objetos misteriosos llamados formalmente como metaclases. La metaclase predeterminada en python es "tipo" si no se especifica

Por defecto, todas las clases definidas son instancias de tipo.

Las clases son instancias de metaclases

Pocos puntos importantes son para entender el comportamiento mencionado

  • Como las clases son instancias de metaclases.
  • Al igual que todos los objetos instanciados, los objetos (instancias) obtienen sus atributos de la clase. La clase obtendrá sus atributos de Meta-Class

Considere seguir el código

class Meta(type):
    def foo(self):
        print(f'foo is called self={self}')
        print('{} is instance of {}: {}'.format(self, Meta, isinstance(self, Meta)))

class C(metaclass=Meta):
    pass

C.foo()

Dónde,

  • clase C es instancia de clase Meta
  • "clase C" es un objeto de clase que es instancia de "clase Meta"
  • Como cualquier otro objeto (instancia) "clase C" tiene acceso a sus atributos / métodos definidos en su clase "clase Meta"
  • Entonces, decodificando "C.foo ()". "C" es una instancia de "Meta" y "foo" es un método que llama a través de una instancia de "Meta" que es "C".
  • El primer argumento del método "foo" es una referencia a la instancia, no a la clase, a diferencia del "método de clase"

Podemos verificar como si "clase C" es una instancia de "Clase Meta

  isinstance(C, Meta)

¿Qué es el método de clase?

Se dice que los métodos de Python están vinculados. Como Python impone la restricción, ese método debe invocarse solo con instancia. A veces, es posible que deseemos invocar métodos directamente a través de la clase sin ninguna instancia (al igual que los miembros estáticos en Java) sin tener que crear ninguna instancia. Se requiere una instancia predeterminada para llamar al método. Como solución alternativa, python proporciona la función incorporada classmethod para vincular el método dado a la clase en lugar de la instancia.

Como los métodos de clase están vinculados a la clase. Se necesita al menos un argumento que haga referencia a la clase en lugar de instancia (self)

si se utiliza la función incorporada / decorador classmethod. El primer argumento será referencia a clase en lugar de instancia

class ClassMethodDemo:
    @classmethod
    def foo(cls):
        print(f'cls is ClassMethodDemo: {cls is ClassMethodDemo}')

Como hemos usado "classmethod" llamamos al método "foo" sin crear ninguna instancia de la siguiente manera

ClassMethodDemo.foo()

La llamada al método anterior devolverá True. Dado que el primer argumento cls es de hecho referencia a "ClassMethodDemo"

Resumen:

  • El primer argumento de Classmethod recibe que es "una referencia a la clase (tradicionalmente denominada cls)"
  • Los métodos de metaclases no son métodos de clase. Los métodos de las metaclases reciben el primer argumento que es "una referencia a la instancia (tradicionalmente denominada self) no a la clase"
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.