Pregunta ¿Qué son las metaclases en Python?


¿Qué son las metaclases y para qué las utilizamos?


4572
2017-09-19 06:10


origen


Respuestas:


Una metaclase es la clase de una clase. Al igual que una clase define cómo se comporta una instancia de la clase, una metaclase define cómo se comporta una clase. Una clase es una instancia de una metaclase.

metaclass diagram

Mientras está en Python, puede usar callables arbitrarios para metaclases (como Jerub muestra), el enfoque más útil es convertirlo en una clase real en sí misma. type es la metaclase habitual en Python. En caso de que te lo estés preguntando, sí, type es en sí misma una clase, y es su propio tipo. No podrás recrear algo así como type puramente en Python, pero Python hace trampa un poco. Para crear su propia metaclase en Python, realmente solo quiere subclasificar type.

Una metaclase se usa más comúnmente como una fábrica de clase. Al igual que crear una instancia de la clase llamando a la clase, Python crea una nueva clase (cuando ejecuta la instrucción 'clase') llamando a la metaclase. Combinado con lo normal __init__ y __new__ Los métodos, las metaclases, por lo tanto, le permiten hacer 'cosas adicionales' al crear una clase, como registrar la nueva clase con algún registro, o incluso reemplazar la clase con algo completamente diferente.

Cuando el class declaración se ejecuta, Python primero ejecuta el cuerpo de la class declaración como un bloque normal de código. El espacio de nombres resultante (un dict) contiene los atributos de la clase-a-ser. La metaclase se determina al observar las clases base de la clase futura (las metaclases son heredadas), al __metaclass__ atributo de la clase-a-ser (si hay) o el __metaclass__ variable global. La metaclase se llama con el nombre, las bases y los atributos de la clase para instanciarla.

Sin embargo, las metaclases realmente definen el tipo de una clase, no solo una fábrica, así que puedes hacer mucho más con ellos. Puede, por ejemplo, definir métodos normales en la metaclase. Estos métodos de metaclase son como class class, en el sentido de que pueden invocarse en la clase sin una instancia, pero tampoco son como class class porque no pueden invocarse en una instancia de la clase. type.__subclasses__() es un ejemplo de un método en el type metaclass. También puede definir los métodos normales de "magia", como __add__, __iter__ y __getattr__, para implementar o cambiar cómo se comporta la clase.

Aquí hay un ejemplo agregado de los pedazos:

def make_hook(f):
    """Decorator to turn 'foo' method into '__foo__'"""
    f.is_hook = 1
    return f

class MyType(type):
    def __new__(mcls, name, bases, attrs):

        if name.startswith('None'):
            return None

        # Go over attributes and see if they should be renamed.
        newattrs = {}
        for attrname, attrvalue in attrs.iteritems():
            if getattr(attrvalue, 'is_hook', 0):
                newattrs['__%s__' % attrname] = attrvalue
            else:
                newattrs[attrname] = attrvalue

        return super(MyType, mcls).__new__(mcls, name, bases, newattrs)

    def __init__(self, name, bases, attrs):
        super(MyType, self).__init__(name, bases, attrs)

        # classregistry.register(self, self.interfaces)
        print "Would register class %s now." % self

    def __add__(self, other):
        class AutoClass(self, other):
            pass
        return AutoClass
        # Alternatively, to autogenerate the classname as well as the class:
        # return type(self.__name__ + other.__name__, (self, other), {})

    def unregister(self):
        # classregistry.unregister(self)
        print "Would unregister class %s now." % self

class MyObject:
    __metaclass__ = MyType


class NoneSample(MyObject):
    pass

# Will print "NoneType None"
print type(NoneSample), repr(NoneSample)

class Example(MyObject):
    def __init__(self, value):
        self.value = value
    @make_hook
    def add(self, other):
        return self.__class__(self.value + other.value)

# Will unregister the class
Example.unregister()

inst = Example(10)
# Will fail with an AttributeError
#inst.unregister()

print inst + inst
class Sibling(MyObject):
    pass

ExampleSibling = Example + Sibling
# ExampleSibling is now a subclass of both Example and Sibling (with no
# content of its own) although it will believe it's called 'AutoClass'
print ExampleSibling
print ExampleSibling.__mro__

2079
2017-09-19 07:01



Clases como objetos

Antes de entender las metaclases, necesita dominar las clases en Python. Y Python tiene una idea muy peculiar de qué clases son, tomadas del lenguaje Smalltalk.

En la mayoría de los idiomas, las clases son solo piezas de código que describen cómo producir un objeto. Eso también es cierto en Python:

>>> class ObjectCreator(object):
...       pass
...

>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>

Pero las clases son más que eso en Python. Las clases también son objetos.

Sí, objetos.

Tan pronto como use la palabra clave class, Python lo ejecuta y crea un objeto. La instrucción

>>> class ObjectCreator(object):
...       pass
...

crea en memoria un objeto con el nombre "ObjectCreator".

Este objeto (la clase) es en sí mismo capaz de crear objetos (las instancias), y esta es la razón por la cual es una clase.

Pero aún así, es un objeto, y por lo tanto:

  • puedes asignarlo a una variable
  • puedes copiarlo
  • puedes agregarle atributos
  • puedes pasarlo como un parámetro de función

p.ej.:

>>> print(ObjectCreator) # you can print a class because it's an object
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print(o)
...
>>> echo(ObjectCreator) # you can pass a class as a parameter
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>

Crear clases dinámicamente

Como las clases son objetos, puedes crearlos sobre la marcha, como cualquier objeto.

Primero, puedes crear una clase en una función usando class:

>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # return the class, not an instance
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # the function returns a class, not an instance
<class '__main__.Foo'>
>>> print(MyClass()) # you can create an object from this class
<__main__.Foo object at 0x89c6d4c>

Pero no es tan dinámico, ya que todavía tiene que escribir toda la clase usted mismo.

Como las clases son objetos, deben ser generadas por algo.

Cuando usas el class palabra clave, Python crea este objeto automáticamente. Pero como con la mayoría de las cosas en Python, te da una forma de hacerlo manualmente.

Recuerde la función type? La buena función antigua que te permite saber qué Escriba un objeto es:

>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>

Bien, type tiene una habilidad completamente diferente, también puede crear clases sobre la marcha. type puede tomar la descripción de una clase como parámetros, y devuelve una clase.

(Lo sé, es una tontería que la misma función pueda tener dos usos completamente diferentes según los parámetros que le pasen. Es un problema debido al retroceso compatibilidad en Python)

type funciona de esta manera:

type(name of the class,
     tuple of the parent class (for inheritance, can be empty),
     dictionary containing attributes names and values)

p.ej.:

>>> class MyShinyClass(object):
...       pass

se puede crear manualmente de esta manera:

>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # create an instance with the class
<__main__.MyShinyClass object at 0x8997cec>

Notarás que usamos "MyShinyClass" como el nombre de la clase y como la variable para contener la referencia de clase. Ellos pueden ser diferentes, pero no hay razón para complicar las cosas.

type acepta un diccionario para definir los atributos de la clase. Asi que:

>>> class Foo(object):
...       bar = True

Puede ser traducido a:

>>> Foo = type('Foo', (), {'bar':True})

Y usado como una clase normal:

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar)
True

Y, por supuesto, puedes heredar de él, así que:

>>>   class FooChild(Foo):
...         pass

sería:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar is inherited from Foo
True

Eventualmente, querrás agregar métodos a tu clase. Solo defina una función con la firma adecuada y asignarlo como un atributo.

>>> def echo_bar(self):
...       print(self.bar)
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

Y puede agregar aún más métodos después de crear dinámicamente la clase, al igual que agregar métodos a un objeto de clase creado normalmente.

>>> def echo_bar_more(self):
...       print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True

Ya ves a dónde vamos: en Python, las clases son objetos, y puedes crear una clase sobre la marcha, dinámicamente.

Esto es lo que hace Python cuando usa la palabra clave class, y lo hace al usar una metaclase.

Qué son las metaclases (finalmente)

Las metaclases son las "cosas" que crean clases.

Usted define clases para crear objetos, ¿verdad?

Pero aprendimos que las clases de Python son objetos.

Bueno, las metaclases son las que crean estos objetos. Son las clases de las clases, puedes imaginarlos de esta manera:

MyClass = MetaClass()
my_object = MyClass()

Ya has visto eso type te permite hacer algo como esto:

MyClass = type('MyClass', (), {})

Es porque la función type es de hecho una metaclase. type es el metaclass Python usa para crear todas las clases detrás de las escenas.

Ahora te preguntas por qué diablos está escrito en minúsculas, y no Type?

Bueno, supongo que es una cuestión de coherencia con str, la clase que crea cuerdas objetos, y intla clase que crea objetos enteros. type es solo la clase que crea objetos de clase.

Usted ve eso al marcar el __class__ atributo.

Todo, y me refiero a todo, es un objeto en Python. Eso incluye enteros, cadenas, funciones y clases. Todos ellos son objetos. Y todos ellos tienen sido creado a partir de una clase:

>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>> foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>

Ahora, ¿qué es el __class__ de cualquier __class__ ?

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>

Entonces, una metaclase es solo lo que crea objetos de clase.

Puede llamarlo una 'fábrica de clase' si lo desea.

type es la metaclase incorporada que usa Python, pero por supuesto, puedes crear metaclass propia.

los __metaclass__ atributo

Puedes agregar un __metaclass__ atributo cuando escribes una clase:

class Foo(object):
    __metaclass__ = something...
    [...]

Si lo hace, Python usará la metaclase para crear la clase Foo.

Cuidado, es complicado.

Usted escribe class Foo(object) primero, pero el objeto de clase Foo no se crea en memoria todavía.

Python buscará __metaclass__ en la definición de clase. Si lo encuentra, lo usará para crear la clase de objeto Foo. Si no lo hace, usará type para crear la clase

Lee eso varias veces.

Cuando tu lo hagas:

class Foo(Bar):
    pass

Python hace lo siguiente:

Hay un __metaclass__ atributo en Foo?

Si es así, crea en memoria un objeto de clase (dije un objeto de clase, quédate aquí conmigo), con el nombre Foo usando lo que está adentro __metaclass__.

Si Python no puede encontrar __metaclass__, buscará un __metaclass__ a nivel de MÓDULO, e intenta hacer lo mismo (pero solo para las clases que no heredan nada, básicamente clases de estilo antiguo).

Entonces, si no puede encontrar __metaclass__ en absoluto, usará el Barmetaclass propio (el primer padre) (que podría ser el predeterminado type) para crear el objeto de clase.

Tenga cuidado aquí que el __metaclass__ atributo no será heredado, la metaclase del padre (Bar.__class__) estarán. Si Bar uso un __metaclass__ atributo que creó Bar con type() (y no type.__new__()), las subclases no heredarán ese comportamiento.

Ahora la gran pregunta es, ¿qué puedes poner en __metaclass__ ?

La respuesta es: algo que puede crear una clase.

¿Y qué puede crear una clase? type, o cualquier cosa que lo subclases o lo use.

Metaclases personalizadas

El objetivo principal de una metaclase es cambiar la clase automáticamente, cuando es creado

Usualmente haces esto para las API, donde quieres crear clases que coincidan con contexto actual.

Imagine un ejemplo estúpido, donde decide que todas las clases en su módulo debería tener sus atributos escritos en mayúsculas. Hay varias formas de haz esto, pero una forma es establecer __metaclass__ a nivel de módulo

De esta forma, todas las clases de este módulo se crearán utilizando esta metaclase, y solo tenemos que decirle a la metaclase que convierta todos los atributos en mayúsculas.

Por suerte, __metaclass__ en realidad puede ser cualquier invocable, no necesita ser un clase formal (lo sé, algo con 'clase' en su nombre no necesita ser una clase, imagínate ... pero es útil).

Entonces comenzaremos con un ejemplo simple, usando una función.

# the metaclass will automatically get passed the same argument
# that you usually pass to `type`
def upper_attr(future_class_name, future_class_parents, future_class_attr):
    """
      Return a class object, with the list of its attribute turned
      into uppercase.
    """

    # pick up any attribute that doesn't start with '__' and uppercase it
    uppercase_attr = {}
    for name, val in future_class_attr.items():
        if not name.startswith('__'):
            uppercase_attr[name.upper()] = val
        else:
            uppercase_attr[name] = val

    # let `type` do the class creation
    return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr # this will affect all classes in the module

class Foo(): # global __metaclass__ won't work with "object" though
    # but we can define __metaclass__ here instead to affect only this class
    # and this will work with "object" children
    bar = 'bip'

print(hasattr(Foo, 'bar'))
# Out: False
print(hasattr(Foo, 'BAR'))
# Out: True

f = Foo()
print(f.BAR)
# Out: 'bip'

Ahora, hagamos exactamente lo mismo, pero usando una clase real para una metaclase:

# remember that `type` is actually a class like `str` and `int`
# so you can inherit from it
class UpperAttrMetaclass(type):
    # __new__ is the method called before __init__
    # it's the method that creates the object and returns it
    # while __init__ just initializes the object passed as parameter
    # you rarely use __new__, except when you want to control how the object
    # is created.
    # here the created object is the class, and we want to customize it
    # so we override __new__
    # you can do some stuff in __init__ too if you wish
    # some advanced use involves overriding __call__ as well, but we won't
    # see this
    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type(future_class_name, future_class_parents, uppercase_attr)

Pero esto no es realmente POO. Llamamos type directamente y no anulamos o llame al padre __new__. Vamos a hacerlo:

class UpperAttrMetaclass(type):

    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        # reuse the type.__new__ method
        # this is basic OOP, nothing magic in there
        return type.__new__(upperattr_metaclass, future_class_name,
                            future_class_parents, uppercase_attr)

Puede haber notado el argumento extra upperattr_metaclass. Ahi esta nada especial al respecto: __new__ siempre recibe la clase en la que está definido, como primer parámetro. Al igual que tu tienes self para los métodos ordinarios que reciben la instancia como primer parámetro, o la clase de definición para los métodos de clase.

Por supuesto, los nombres que utilicé aquí son largos por el bien de la claridad, pero al igual para self, todos los argumentos tienen nombres convencionales. Entonces una producción real metaclass se vería así:

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type.__new__(cls, clsname, bases, uppercase_attr)

Podemos hacerlo aún más limpio mediante el uso de super, lo que facilitará la herencia (porque sí, puede tener metaclases, heredar de metaclases, heredar de tipo):

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attr)

Eso es. Realmente no hay nada más sobre metaclases.

La razón detrás de la complejidad del código que usa metaclases no es porque de metaclases, es porque usas metaclases para hacer cosas retorcidas confiando en la introspección, manipulando la herencia, vars como __dict__, etc.

De hecho, las metaclases son especialmente útiles para hacer magia negra, y por lo tanto cosas complicadas Pero por sí mismos, son simples:

  • interceptar una creación de clase
  • modificar la clase
  • devolver la clase modificada

¿Por qué usarías clases de metaclases en lugar de funciones?

Ya que __metaclass__ puede aceptar cualquier invocable, ¿por qué usaría una clase? ya que obviamente es más complicado?

Hay varias razones para hacerlo:

  • La intención es clara. Cuando lees UpperAttrMetaclass(type), ya sabes lo que va a seguir
  • Puedes usar OOP. Metaclass puede heredar de metaclass, anular los métodos principales. Las metaclases incluso pueden usar metaclases.
  • Las subclases de una clase serán instancias de su metaclase si especificó una clase de metaclase, pero no con una función de metaclase.
  • Puedes estructurar mejor tu código. Nunca usas metaclases para algo como trivial como el ejemplo anterior. Por lo general, es para algo complicado. Tener el capacidad de hacer varios métodos y agruparlos en una clase es muy útil para hacer que el código sea más fácil de leer.
  • Puedes enganchar __new__, __init__ y __call__. Lo cual permitirá tu para hacer cosas diferentes Incluso si usualmente puedes hacerlo todo en __new__, algunas personas simplemente se sienten más cómodas usando __init__.
  • Estos se llaman metaclases, ¡maldita sea! Debe significar algo!

¿Por qué usarías metaclases?

Ahora la gran pregunta. ¿Por qué usarías alguna función oscura propensa a errores?

Bueno, por lo general, no:

Las metaclases son magia más profunda que   El 99% de los usuarios nunca deberían preocuparse.   Si te preguntas si los necesitas,   usted no (las personas que realmente   necesito que sepan con certeza que   los necesitan y no necesitan una   explicación sobre por qué).

Python Guru Tim Peters

El caso de uso principal para una metaclase es crear una API. Un ejemplo típico de esto es el Django ORM.

Te permite definir algo como esto:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

Pero si haces esto:

guy = Person(name='bob', age='35')
print(guy.age)

No devolverá un IntegerField objeto. Devolverá un int, e incluso puede tomarlo directamente de la base de datos.

Esto es posible porque models.Model define __metaclass__ y usa algo de magia que convertirá el Person usted acaba de definir con declaraciones simples en un gancho complejo a un campo de base de datos.

Django hace que algo complejo parezca simple al exponer una API simple y el uso de metaclases, recreando el código de esta API para hacer el trabajo real entre bastidores.

La última palabra

En primer lugar, usted sabe que las clases son objetos que pueden crear instancias.

Bueno, de hecho, las clases son en sí mismas instancias. De metaclases

>>> class Foo(object): pass
>>> id(Foo)
142630324

Todo es un objeto en Python, y todos son instancias de clases o instancias de metaclases.

Excepto por type.

type es en realidad su propia metaclase. Esto no es algo que podrías reproducirse en Python puro, y se hace haciendo trampa un poco en la implementación nivel.

En segundo lugar, las metaclases son complicadas. Puede que no quieras usarlos para alteraciones de clase muy simples. Puedes cambiar las clases usando dos técnicas diferentes:

El 99% del tiempo necesitas una alteración de clase, es mejor que los uses.

Pero el 98% del tiempo, no necesitas alteración de clase en absoluto.


5755
2017-09-19 06:26



Tenga en cuenta que esta respuesta es para Python 2.x, ya que fue escrita en 2008, las metaclases son ligeramente diferentes en 3.x, vea los comentarios.

Las metaclases son la salsa secreta que hace que la 'clase' funcione. La metaclase predeterminada para un nuevo objeto de estilo se llama 'tipo'.

class type(object)
  |  type(object) -> the object's type
  |  type(name, bases, dict) -> a new type

Las metaclases toman 3 args. 'nombre','bases'y'dict'

Aquí es donde comienza el secreto. Busque de dónde vienen el nombre, las bases y el dict en esta definición de clase de ejemplo.

class ThisIsTheName(Bases, Are, Here):
    All_the_code_here
    def doesIs(create, a):
        dict

Vamos a definir una metaclase que demostrará cómo 'clase:'lo llama.

def test_metaclass(name, bases, dict):
    print 'The Class Name is', name
    print 'The Class Bases are', bases
    print 'The dict has', len(dict), 'elems, the keys are', dict.keys()

    return "yellow"

class TestName(object, None, int, 1):
    __metaclass__ = test_metaclass
    foo = 1
    def baz(self, arr):
        pass

print 'TestName = ', repr(TestName)

# output => 
The Class Name is TestName
The Class Bases are (<type 'object'>, None, <type 'int'>, 1)
The dict has 4 elems, the keys are ['baz', '__module__', 'foo', '__metaclass__']
TestName =  'yellow'

Y ahora, un ejemplo que realmente significa algo, esto hará que las variables en la lista "atributos" se establezcan automáticamente en la clase y se establezcan en Ninguno.

def init_attributes(name, bases, dict):
    if 'attributes' in dict:
        for attr in dict['attributes']:
            dict[attr] = None

    return type(name, bases, dict)

class Initialised(object):
    __metaclass__ = init_attributes
    attributes = ['foo', 'bar', 'baz']

print 'foo =>', Initialised.foo
# output=>
foo => None

Tenga en cuenta que el comportamiento mágico que 'Initalised' gana al tener la metaclase init_attributes no se pasa a una subclase de Initalised.

Aquí hay un ejemplo aún más concreto, que muestra cómo puede subclase 'escribir' para hacer una metaclase que realiza una acción cuando se crea la clase. Esto es bastante complicado:

class MetaSingleton(type):
    instance = None
    def __call__(cls, *args, **kw):
        if cls.instance is None:
            cls.instance = super(MetaSingleton, cls).__call__(*args, **kw)
        return cls.instance

 class Foo(object):
     __metaclass__ = MetaSingleton

 a = Foo()
 b = Foo()
 assert a is b

311
2017-09-19 06:45



Un uso para las metaclases es agregar nuevas propiedades y métodos a una instancia de forma automática.

Por ejemplo, si miras Modelos de Django, su definición parece un poco confusa. Parece que solo está definiendo propiedades de clase:

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

Sin embargo, en tiempo de ejecución, los objetos Persona se llenan con todo tipo de métodos útiles. Ver el fuente para una metaclase increíble.


124
2018-06-21 16:30



Otros han explicado cómo funcionan las metaclases y cómo encajan en el sistema de tipo Python. Aquí hay un ejemplo de para qué se pueden usar. En un marco de prueba que escribí, quería hacer un seguimiento del orden en el que se definían las clases, para poder luego instanciarlas en este orden. Me pareció más fácil hacer esto usando una metaclase.

class MyMeta(type):

    counter = 0

    def __init__(cls, name, bases, dic):
        type.__init__(cls, name, bases, dic)
        cls._order = MyMeta.counter
        MyMeta.counter += 1

class MyType(object):              # Python 2
    __metaclass__ = MyMeta

class MyType(metaclass=MyMeta):    # Python 3
    pass

Cualquier cosa que sea una subclase de MyType luego obtiene un atributo de clase _order que registra el orden en que se definieron las clases.


117
2017-09-19 06:32



Creo que la introducción de ONLamp a la programación de metaclases está bien escrita y ofrece una muy buena introducción al tema a pesar de tener ya varios años.

http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html

En resumen: una clase es un modelo para la creación de una instancia, una metaclase es un modelo para la creación de una clase. Se puede ver fácilmente que en Python las clases también deben ser objetos de primera clase para permitir este comportamiento.

Nunca he escrito uno, pero creo que uno de los mejores usos de las metaclases se puede ver en el Marco Django. Las clases modelo usan un enfoque de metaclase para permitir un estilo declarativo de escribir nuevos modelos o formar clases. Mientras que la metaclase está creando la clase, todos los miembros tienen la posibilidad de personalizar la clase en sí.

Lo que queda por decir es: si no sabes qué metaclases son, la probabilidad de que no los necesitará es 99%


86
2017-08-10 23:28



¿Qué son las metaclases? ¿Para qué los utiliza?

TLDR: una metaclase crea una instancia y define el comportamiento de una clase al igual que una clase crea una instancia y define el comportamiento de una instancia.

Pseudocódigo:

>>> Class(...)
instance

Lo anterior debería ser familiar. Bueno, ¿dónde Class ¿viene de? Es una instancia de una metaclase (también pseudocódigo):

>>> Metaclass(...)
Class

En código real, podemos pasar la metaclase predeterminada type, todo lo que necesitamos para instanciar una clase y obtenemos una clase:

>>> type('Foo', (object,), {}) # requires a name, bases, and a namespace
<class '__main__.Foo'>

Poniéndolo de manera diferente

  • Una clase es para una instancia como una metaclase para una clase.

    Cuando instanciamos un objeto, obtenemos una instancia:

    >>> object()                          # instantiation of class
    <object object at 0x7f9069b4e0b0>     # instance
    

    Del mismo modo, cuando definimos una clase explícitamente con la metaclase predeterminada, type, lo instanciamos:

    >>> type('Object', (object,), {})     # instantiation of metaclass
    <class '__main__.Object'>             # instance
    
  • Dicho de otra manera, una clase es una instancia de una metaclase:

    >>> isinstance(object, type)
    True
    
  • Dicho de otra manera, una metaclase es la clase de una clase.

    >>> type(object) == type
    True
    >>> object.__class__
    <class 'type'>
    

Cuando escribe una definición de clase y Python la ejecuta, usa una metaclase para instanciar el objeto de clase (que, a su vez, se usará para instanciar instancias de esa clase).

Del mismo modo que podemos usar definiciones de clase para cambiar el comportamiento de las instancias de objetos personalizados, podemos usar una definición de clase de metaclase para cambiar la forma en que se comporta un objeto de clase.

¿Para qué se pueden usar? Desde el documentos:

Los posibles usos de las metaclases son ilimitados. Algunas de las ideas que se han explorado incluyen el registro, la comprobación de interfaces, la delegación automática, la creación automática de propiedades, los proxies, los marcos y el bloqueo / sincronización automática de recursos.

Sin embargo, generalmente se recomienda a los usuarios evitar el uso de metaclases a menos que sea absolutamente necesario.

Usas una metaclase cada vez que creas una clase:

Cuando escribe una definición de clase, por ejemplo, así,

class Foo(object): 
    'demo'

Crea una instancia de un objeto de clase.

>>> Foo
<class '__main__.Foo'>
>>> isinstance(Foo, type), isinstance(Foo, object)
(True, True)

Es lo mismo que llamar funcionalmente type con los argumentos apropiados y asignando el resultado a una variable de ese nombre:

name = 'Foo'
bases = (object,)
namespace = {'__doc__': 'demo'}
Foo = type(name, bases, namespace)

Tenga en cuenta que algunas cosas se agregan automáticamente a la __dict__, es decir, el espacio de nombres:

>>> Foo.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, 
'__module__': '__main__', '__weakref__': <attribute '__weakref__' 
of 'Foo' objects>, '__doc__': 'demo'})

los metaclass del objeto que creamos, en ambos casos, es type.

(Una nota al margen sobre el contenido de la clase __dict__: __module__ está ahí porque las clases deben saber dónde están definidas, y __dict__ y __weakref__ están ahí porque no definimos __slots__ - si nosotros definir __slots__ ahorraremos un poco de espacio en las instancias, ya que no podemos permitir __dict__ y __weakref__ al excluirlos. Por ejemplo:

>>> Baz = type('Bar', (object,), {'__doc__': 'demo', '__slots__': ()})
>>> Baz.__dict__
mappingproxy({'__doc__': 'demo', '__slots__': (), '__module__': '__main__'})

... pero yo divago.)

Podemos extender type como cualquier otra definición de clase:

Aquí está el predeterminado __repr__ de clases:

>>> Foo
<class '__main__.Foo'>

Una de las cosas más valiosas que podemos hacer por defecto al escribir un objeto de Python es proporcionarle un buen __repr__. Cuando llamamos help(repr) aprendemos que hay una buena prueba para un __repr__ eso también requiere una prueba de igualdad - obj == eval(repr(obj)). La siguiente implementación simple de __repr__ y __eq__ para las instancias de clase de nuestra clase de tipo nos proporciona una demostración que puede mejorar el valor predeterminado __repr__ de clases:

class Type(type):
    def __repr__(cls):
        """
        >>> Baz
        Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
        >>> eval(repr(Baz))
        Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
        """
        metaname = type(cls).__name__
        name = cls.__name__
        parents = ', '.join(b.__name__ for b in cls.__bases__)
        if parents:
            parents += ','
        namespace = ', '.join(': '.join(
          (repr(k), repr(v) if not isinstance(v, type) else v.__name__))
               for k, v in cls.__dict__.items())
        return '{0}(\'{1}\', ({2}), {{{3}}})'.format(metaname, name, parents, namespace)
    def __eq__(cls, other):
        """
        >>> Baz == eval(repr(Baz))
        True            
        """
        return (cls.__name__, cls.__bases__, cls.__dict__) == (
                other.__name__, other.__bases__, other.__dict__)

Entonces, cuando creamos un objeto con esta metaclase, __repr__ repetido en la línea de comando proporciona una vista mucho menos fea que la predeterminada:

>>> class Bar(object): pass
>>> Baz = Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
>>> Baz
Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})

Con un buen __repr__ definido para la instancia de clase, tenemos una mayor capacidad para depurar nuestro código. Sin embargo, mucha más verificación con eval(repr(Class)) es poco probable (ya que las funciones serían bastante imposibles de evaluar desde su predeterminado __repr__es).

Un uso esperado: __prepare__ un espacio de nombres

Si, por ejemplo, queremos saber en qué orden se crean los métodos de una clase, podríamos proporcionar un dict ordenado como el espacio de nombres de la clase. Nosotros haríamos esto con __prepare__ cual devuelve el dict de espacio de nombres para la clase si se implementa en Python 3:

from collections import OrderedDict

class OrderedType(Type):
    @classmethod
    def __prepare__(metacls, name, bases, **kwargs):
        return OrderedDict()
    def __new__(cls, name, bases, namespace, **kwargs):
        result = Type.__new__(cls, name, bases, dict(namespace))
        result.members = tuple(namespace)
        return result

Y el uso:

class OrderedMethodsObject(object, metaclass=OrderedType):
    def method1(self): pass
    def method2(self): pass
    def method3(self): pass
    def method4(self): pass

Y ahora tenemos un registro del orden en que se crearon estos métodos (y otros atributos de clase):

>>> OrderedMethodsObject.members
('__module__', '__qualname__', 'method1', 'method2', 'method3', 'method4')

Tenga en cuenta que este ejemplo fue adaptado de documentación - el nuevo enum en la biblioteca estándar Haz esto.

Entonces lo que hicimos fue instanciar una metaclase creando una clase. También podemos tratar la metaclase como lo haríamos con cualquier otra clase. Tiene un orden de resolución de método:

>>> inspect.getmro(OrderedType)
(<class '__main__.OrderedType'>, <class '__main__.Type'>, <class 'type'>, <class 'object'>)

Y tiene aproximadamente lo correcto repr (que ya no podemos evaluar a menos que podamos encontrar una manera de representar nuestras funciones):

>>> OrderedMethodsObject
OrderedType('OrderedMethodsObject', (object,), {'method1': <function OrderedMethodsObject.method1 at 0x0000000002DB01E0>, 'members': ('__module__', '__qualname__', 'method1', 'method2', 'method3', 'method4'), 'method3': <function OrderedMet
hodsObject.method3 at 0x0000000002DB02F0>, 'method2': <function OrderedMethodsObject.method2 at 0x0000000002DB0268>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'OrderedMethodsObject' objects>, '__doc__': None, '__d
ict__': <attribute '__dict__' of 'OrderedMethodsObject' objects>, 'method4': <function OrderedMethodsObject.method4 at 0x0000000002DB0378>})

74
2018-03-01 19:48



Actualización de Python 3

Hay (en este punto) dos métodos clave en una metaclase:

  • __prepare__y
  • __new__

__prepare__ le permite suministrar un mapeo personalizado (como un OrderedDict) para ser utilizado como el espacio de nombres mientras se crea la clase. Debe devolver una instancia del espacio de nombre que elija. Si no implementa __prepare__una normal dict es usado.

__new__ es responsable de la creación / modificación real de la clase final.

Una metaclass bare-bones, do-nothing-extra le gustaría:

class Meta(type):

    def __prepare__(metaclass, cls, bases):
        return dict()

    def __new__(metacls, cls, bases, clsdict):
        return super().__new__(metacls, cls, bases, clsdict)

Un simple ejemplo:

Digamos que quiere que se ejecute un código de validación simple en sus atributos, como siempre debe ser un int o una str. Sin una metaclase, su clase se vería algo así como:

class Person:
    weight = ValidateType('weight', int)
    age = ValidateType('age', int)
    name = ValidateType('name', str)

Como puede ver, debe repetir el nombre del atributo dos veces. Esto hace posible errores tipográficos junto con errores irritantes.

Una metaclase simple puede abordar ese problema:

class Person(metaclass=Validator):
    weight = ValidateType(int)
    age = ValidateType(int)
    name = ValidateType(str)

Así es como se vería la metaclase (sin usar __prepare__ ya que no es necesario):

class Validator(type):
    def __new__(metacls, cls, bases, clsdict):
        # search clsdict looking for ValidateType descriptors
        for name, attr in clsdict.items():
            if isinstance(attr, ValidateType):
                attr.name = name
                attr.attr = '_' + name
        # create final class and return it
        return super().__new__(metacls, cls, bases, clsdict)

Una muestra de ejecución de:

p = Person()
p.weight = 9
print(p.weight)
p.weight = '9'

produce:

9
Traceback (most recent call last):
  File "simple_meta.py", line 36, in <module>
    p.weight = '9'
  File "simple_meta.py", line 24, in __set__
    (self.name, self.type, value))
TypeError: weight must be of type(s) <class 'int'> (got '9')

Nota: Este ejemplo es bastante simple, también podría haberse logrado con un decorador de clases, pero presumiblemente una metaclase real estaría haciendo mucho más.

La clase 'ValidateType' para referencia:

class ValidateType:
    def __init__(self, type):
        self.name = None  # will be set by metaclass
        self.attr = None  # will be set by metaclass
        self.type = type
    def __get__(self, inst, cls):
        if inst is None:
            return self
        else:
            return inst.__dict__[self.attr]
    def __set__(self, inst, value):
        if not isinstance(value, self.type):
            raise TypeError('%s must be of type(s) %s (got %r)' %
                    (self.name, self.type, value))
        else:
            inst.__dict__[self.attr] = value

55
2017-10-13 09:21



Una metaclase es una clase que indica cómo (alguna) otra clase debería crearse.

Este es un caso en el que vi metaclass como una solución a mi problema: Tenía un problema realmente complicado, que probablemente podría haber sido resuelto de manera diferente, pero elegí resolverlo usando una metaclase. Debido a la complejidad, es uno de los pocos módulos que he escrito donde los comentarios en el módulo superan la cantidad de código que se ha escrito. Aquí está...

#!/usr/bin/env python

# Copyright (C) 2013-2014 Craig Phillips.  All rights reserved.

# This requires some explaining.  The point of this metaclass excercise is to
# create a static abstract class that is in one way or another, dormant until
# queried.  I experimented with creating a singlton on import, but that did
# not quite behave how I wanted it to.  See now here, we are creating a class
# called GsyncOptions, that on import, will do nothing except state that its
# class creator is GsyncOptionsType.  This means, docopt doesn't parse any
# of the help document, nor does it start processing command line options.
# So importing this module becomes really efficient.  The complicated bit
# comes from requiring the GsyncOptions class to be static.  By that, I mean
# any property on it, may or may not exist, since they are not statically
# defined; so I can't simply just define the class with a whole bunch of
# properties that are @property @staticmethods.
#
# So here's how it works:
#
# Executing 'from libgsync.options import GsyncOptions' does nothing more
# than load up this module, define the Type and the Class and import them
# into the callers namespace.  Simple.
#
# Invoking 'GsyncOptions.debug' for the first time, or any other property
# causes the __metaclass__ __getattr__ method to be called, since the class
# is not instantiated as a class instance yet.  The __getattr__ method on
# the type then initialises the class (GsyncOptions) via the __initialiseClass
# method.  This is the first and only time the class will actually have its
# dictionary statically populated.  The docopt module is invoked to parse the
# usage document and generate command line options from it.  These are then
# paired with their defaults and what's in sys.argv.  After all that, we
# setup some dynamic properties that could not be defined by their name in
# the usage, before everything is then transplanted onto the actual class
# object (or static class GsyncOptions).
#
# Another piece of magic, is to allow command line options to be set in
# in their native form and be translated into argparse style properties.
#
# Finally, the GsyncListOptions class is actually where the options are
# stored.  This only acts as a mechanism for storing options as lists, to
# allow aggregation of duplicate options or options that can be specified
# multiple times.  The __getattr__ call hides this by default, returning the
# last item in a property's list.  However, if the entire list is required,
# calling the 'list()' method on the GsyncOptions class, returns a reference
# to the GsyncListOptions class, which contains all of the same properties
# but as lists and without the duplication of having them as both lists and
# static singlton values.
#
# So this actually means that GsyncOptions is actually a static proxy class...
#
# ...And all this is neatly hidden within a closure for safe keeping.
def GetGsyncOptionsType():
    class GsyncListOptions(object):
        __initialised = False

    class GsyncOptionsType(type):
        def __initialiseClass(cls):
            if GsyncListOptions._GsyncListOptions__initialised: return

            from docopt import docopt
            from libgsync.options import doc
            from libgsync import __version__

            options = docopt(
                doc.__doc__ % __version__,
                version = __version__,
                options_first = True
            )

            paths = options.pop('<path>', None)
            setattr(cls, "destination_path", paths.pop() if paths else None)
            setattr(cls, "source_paths", paths)
            setattr(cls, "options", options)

            for k, v in options.iteritems():
                setattr(cls, k, v)

            GsyncListOptions._GsyncListOptions__initialised = True

        def list(cls):
            return GsyncListOptions

        def __getattr__(cls, name):
            cls.__initialiseClass()
            return getattr(GsyncListOptions, name)[-1]

        def __setattr__(cls, name, value):
            # Substitut option names: --an-option-name for an_option_name
            import re
            name = re.sub(r'^__', "", re.sub(r'-', "_", name))
            listvalue = []

            # Ensure value is converted to a list type for GsyncListOptions
            if isinstance(value, list):
                if value:
                    listvalue = [] + value
                else:
                    listvalue = [ None ]
            else:
                listvalue = [ value ]

            type.__setattr__(GsyncListOptions, name, listvalue)

    # Cleanup this module to prevent tinkering.
    import sys
    module = sys.modules[__name__]
    del module.__dict__['GetGsyncOptionsType']

    return GsyncOptionsType

# Our singlton abstract proxy class.
class GsyncOptions(object):
    __metaclass__ = GetGsyncOptionsType()

41
2017-08-09 18:49



Papel de una metaclass __call__() método al crear una instancia de clase

Si ha realizado la programación de Python durante más de unos meses, eventualmente tropezará con un código que se ve así:

# define a class
class SomeClass(object):
    # ...
    # some definition here ...
    # ...

# create an instance of it
instance = SomeClass()

# then call the object as if it's a function
result = instance('foo', 'bar')

Esto último es posible cuando implementa el __call__() método mágico en la clase.

class SomeClass(object):
    # ...
    # some definition here ...
    # ...

    def __call__(self, foo, bar):
        return bar + foo

los __call__() El método se invoca cuando una instancia de una clase se utiliza como invocable. Pero como hemos visto en respuestas anteriores, una clase en sí misma es una instancia de una metaclase, así que cuando usamos la clase como invocable (es decir, cuando creamos una instancia de la misma) estamos llamando a su metaclase __call__() método. En este punto, la mayoría de los programadores de Python están un poco confundidos porque les dijeron que al crear una instancia como esta instance = SomeClass() estás llamando es __init__() método. Algunos que han cavado un poco más profundo saben que antes __init__() hay __new__(). Bueno, hoy se revela otra capa de verdad, antes __new__() está la metaclass __call__().

Estudiemos la cadena de llamadas al método desde específicamente la perspectiva de crear una instancia de una clase.

Esta es una metaclase que registra exactamente el momento antes de que se cree una instancia y el momento en que está a punto de devolverla.

class Meta_1(type):
    def __call__(cls):
        print "Meta_1.__call__() before creating an instance of ", cls
        instance = super(Meta_1, cls).__call__()
        print "Meta_1.__call__() about to return instance."
        return instance

Esta es una clase que usa esa metaclase

class Class_1(object):

    __metaclass__ = Meta_1

    def __new__(cls):
        print "Class_1.__new__() before creating an instance."
        instance = super(Class_1, cls).__new__(cls)
        print "Class_1.__new__() about to return instance."
        return instance

    def __init__(self):
        print "entering Class_1.__init__() for instance initialization."
        super(Class_1,self).__init__()
        print "exiting Class_1.__init__()."

Y ahora vamos a crear una instancia de Class_1

instance = Class_1()
# Meta_1.__call__() before creating an instance of <class '__main__.Class_1'>.
# Class_1.__new__() before creating an instance.
# Class_1.__new__() about to return instance.
# entering Class_1.__init__() for instance initialization.
# exiting Class_1.__init__().
# Meta_1.__call__() about to return instance.

El código anterior en realidad no hace nada más que registrar la tarea y luego delegar el trabajo real al padre (es decir, mantener el comportamiento predeterminado). Entonces con type siendo Meta_1'clase de padres', podemos imaginar que esta sería la pseudo implementación de type.__call__():

class type:
    def __call__(cls, *args, **kwarg):

        # ... maybe a few things done to cls here

        # then we call __new__() on the class to create an instance
        instance = cls.__new__(cls, *args, **kwargs)

        # ... maybe a few things done to the instance here

        # then we initialize the instance with its __init__() method
        instance.__init__(*args, **kwargs)

        # ... maybe a few more things done to instance here

        # then we return it
        return instance

Podemos ver que la metaclass __call__() método es el que se llama primero. Luego delega la creación de la instancia a la clase __new__()método e inicialización a la instancia __init__(). También es el que finalmente devuelve la instancia.

De lo anterior se deduce que la metaclass __call__() también se le da la oportunidad de decidir si una llamada a Class_1.__new__() o Class_1.__init__() eventualmente se realizará. En el transcurso de su ejecución, podría devolver un objeto que no haya sido tocado por ninguno de estos métodos. Tomemos, por ejemplo, este enfoque del patrón singleton:

class Meta_2(type):
    singletons = {}

    def __call__(cls, *args, **kwargs):
        if cls in Meta_2.singletons:
            # we return the only instance and skip a call to __new__() 
            # and __init__()
            print ("{} singleton returning from Meta_2.__call__(), "
                   "skipping creation of new instance.".format(cls))
            return Meta_2.singletons[cls]

        # else if the singleton isn't present we proceed as usual
        print "Meta_2.__call__() before creating an instance."
        instance = super(Meta_2, cls).__call__(*args, **kwargs)
        Meta_2.singletons[cls] = instance
        print "Meta_2.__call__() returning new instance."
        return instance

class Class_2(object):

    __metaclass__ = Meta_2

    def __new__(cls, *args, **kwargs):
        print "Class_2.__new__() before creating instance."
        instance = super(Class_2, cls).__new__(cls)
        print "Class_2.__new__() returning instance."
        return instance

    def __init__(self, *args, **kwargs):
        print "entering Class_2.__init__() for initialization."
        super(Class_2, self).__init__()
        print "exiting Class_2.__init__()."

Observemos lo que sucede cuando intentamos crear un objeto de tipo repetidamente Class_2

a = Class_2()
# Meta_2.__call__() before creating an instance.
# Class_2.__new__() before creating instance.
# Class_2.__new__() returning instance.
# entering Class_2.__init__() for initialization.
# exiting Class_2.__init__().
# Meta_2.__call__() returning new instance.

b = Class_2()
# <class '__main__.Class_2'> singleton returning from Meta_2.__call__(), skipping creation of new instance.

c = Class_2()
# <class '__main__.Class_2'> singleton returning from Meta_2.__call__(), skipping creation of new instance.

a is b is c # True

37
2017-12-27 02:21