Metaclases de Python

El concepto clave de python son los objetos. Casi todo en python es un objeto, que incluye funciones y clases. Como resultado, las funciones y las clases se pueden pasar como argumentos, pueden existir como una instancia, etc. Sobre todo, el concepto de objetos permite que las clases generen otras clases.

Las clases que generan otras clases se definen como metaclases. En esta sección, discutiremos el concepto de metaclases y las formas específicas de usarlas. En esta sección, cubriremos los siguientes temas:

  • escribe
  • Escribir metaclases
  • Casos de uso de metaclases

escribe

Una clase define las propiedades y acciones disponibles de su objeto y también actúa como una fábrica para la creación de objetos. Entendamos el proceso creando una clase usando type directamente. La clase exacta que se utiliza para la creación de instancias de clase se denomina tipo. Normalmente, definimos una clase usando una sintaxis especial llamada palabra clave class , pero esta sintaxis es un sustituto de type class . Ilustremos con un ejemplo:

 En primer lugar, analizamos el escenario de la creación de una clase utilizando la palabra clave class . Verifiquemos el siguiente código:

Python3

class FoodType(object):
  def __init__(self, ftype):
    self.ftype = ftype
      
  def getFtype(self):
    return self.ftype
    
  
def main():
  fType = FoodType(ftype = 'Vegetarian')
  print(fType.getFtype())
    
main()
Producción

Vegetarian



Aquí hemos creado una clase llamada FoodType usando la palabra clave class . Esta palabra clave de clase actúa como un sustituto de la sintaxis de tipo. Ahora veamos cómo usar la palabra clave tipo e. Vayamos a través del siguiente código:

Python3

def init(self, ftype):
    self.ftype = ftype
  
def getFtype(self):
    return self.ftype 
  
FoodType = type('FoodType', (object, ), {
    '__init__': init,
    'getFtype' : getFtype,
    })
  
fType = FoodType(ftype ='Vegetarian')
print(fType.getFtype())
Producción

Vegetarian



Centrémonos en el tipo . Tiene tres argumentos que son los siguientes:

  • El primer argumento es una string: FoodType . Esta string se asigna como el nombre de la clase.
  • El segundo argumento es una tupla – (objeto, ) . Esto indica que la clase FoodType hereda de la clase de objeto. Aquí, la coma final ayuda al intérprete de Python a reconocerlo como una tupla.
  • Aquí, el tercer argumento es un diccionario que menciona el atributo de una clase. En este caso, la clase tiene dos métodos: init y getFtype.

Crear una subclase usando tipo

Veamos el escenario normal de creación de una subclase, es decir, usando la palabra clave class. Aquí, crearemos una subclase VegType en la que la clase principal es FoodType .

Python3

class FoodType(object):
  def __init__(self, ftype):
    self.ftype = ftype
      
  def getFtype(self):
    return self.ftype
    
class VegType(FoodType):
  def vegFoods(self):
    return {'Spinach', 'Bitter Guard'}
    
def main():
  vType = VegType(ftype = 'Vegetarian')
  print(vType.getFtype())
  print(vType.vegFoods())
    
main()
Producción

Vegetarian
{'Spinach', 'Bitter Guard'}



Ahora veamos cómo convertir el código anterior usando type . Para la clase FoodType , el segundo argumento del tipo es la clase de objeto, la superclase, es decir, FoodType es la subclase de la clase Object. De manera similar, la clase VegType es la subclase de FoodType y, por lo tanto, al crear la clase VegType , el segundo argumento de tipo se refiere a la clase FoodType

Python3

def init(self, ftype):
    self.ftype = ftype
  
def getFtype(self):
    return self.ftype 
  
FoodType = type('FoodType', (object, ), {
    '__init__': init,
    'getFtype' : getFtype,
    })
  
def vegFoods(self):
    return {'Spinach', 'Bitter Guard'}
   
## creating subclass using type
VegType = type('VegType', (FoodType, ), {
    'vegFoods' : vegFoods,
    })
  
  
vType = VegType(ftype ='Vegetarian')
print(vType.getFtype())
print(vType.vegFoods())
Producción

Vegetarian
{'Spinach', 'Bitter Guard'}



Escribir metaclases

Las metaclases son clases que heredan directamente del tipo. El método que deberían implementar las metaclases personalizadas es el método __new__. Los argumentos mencionados en el método __nuevo__ de las metaclases se reflejan en el método __nuevo__ de la clase de tipos. Tiene cuatro argumentos posicionales. Son los siguientes:

  1. El primer argumento es la propia metaclase.
  2. El segundo argumento es el nombre de la clase.
  3. El tercer argumento son las superclases (en forma de tupla)
  4. El cuarto argumento son los atributos de clase (en forma de diccionario)

 Echemos un vistazo al siguiente código.

Python3

class MetaCls(type):
    """A sample metaclass without any functionality"""
    def __new__(cls, clsname, superclasses, attributedict):
        print("clsname:", clsname)
        print("superclasses:", superclasses)
        print("attrdict:", attributedict)
        return super(MetaCls, cls).__new__(cls, \
                       clsname, superclasses, attributedict)
  
C = MetaCls('C', (object, ), {})
print("class type:", type(C))
Producción

clsname: C
superclasses: (<class 'object'>, )
attrdict: {}
class type: <class '__main__.MetaCls'>



Puede notar que el tipo de clase C es una metaclase: MetaCls . Comprobemos el tipo de una clase normal.

Python3

class S(object):
  pass
  
print(type(S))
Producción

<class 'type'>



Herencia de metaclases

Veamos, cómo heredar de metaclases.

Python3

class MetaCls(type):
    """A sample metaclass without any functionality"""
    def __new__(cls, clsname, supercls, attrdict):
          
        return super(MetaCls, cls).__new__(cls, clsname, supercls, attrdict)
  
C = MetaCls('C', (object, ), {})
## class A inherits from MetaCls       
class A(C):
  pass
  
print(type(A))
Producción

<class '__main__.MetaCls'>



En este caso, puede ver que la clase A es una instancia de una metaclase. Esto se debe a que su superclase C es una instancia de una metaclase. Ahora consideraremos el escenario donde las subclases de clase tienen dos o más clases distintas. Veamos el siguiente código de ejemplo:

Python3

class MetaCls(type):
    """A sample metaclass without any functionality"""
    def __new__(cls, clsname, supercls, attrdict):
          
        return super(MetaCls, cls).__new__(cls, clsname, supercls, attrdict)
  
## a class of the type metclass
A = MetaCls('A', (object, ), {})
print('Type of class A:', type(A))
  
class B(object):
    pass
print('Type of class B:', type(B))
  
## class C inherits from both the class, A and B
class C(A, B):
    pass
print('Type of class C:', type(C))
Producción

Type of class A: <class '__main__.MetaCls'>
Type of class B: <class 'type'>
Type of class C: <class '__main__.MetaCls'>



Aquí, puede ver que la clase C se hereda de la clase A (metaclase) y la clase B (clase de tipo). Pero aquí el tipo de clase C es una metaclase. Esto se debe a que cuando el intérprete de Python verificó las superclases, descubrió que la metaclase es una subclase del tipo en sí. Entonces consideró metaclase como el tipo de clase C para evitar cualquier conflicto.  

Veamos otro código de muestra donde una clase hereda de dos metaclases diferentes.

Python3

class MetaCls(type):
    """A sample metaclass without any functionality"""
    def __new__(cls, clsname, supercls, attrdict):
          
        return super(MetaCls, cls).__new__(cls, clsname, supercls, attrdict)
  
A = MetaCls('A', (object, ), {})
  
  
class NewMetaCls(type):
    """A sample metaclass without any functionality"""
    def __new__(cls, clsname, supercls, attrdict):
          
        return super(NewMetaCls, cls).__new__(cls, clsname, supercls, attrdict)
  
NewA = NewMetaCls('NewA', (object, ), {})
  
class C(A, NewA):
  pass

Aquí obtendrá el siguiente mensaje de error al intentar heredar de dos metaclases diferentes.

Traceback (most recent call last):
  File "/home/eb81e4ecb05868f83d5f375ffc78e237.py", line 18, in <module>
    class C(A, NewA):
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) 
subclass of the metaclasses of all its bases


Esto se debe a que Python solo puede tener una metaclase para una clase. Aquí, la clase C no puede heredar de dos metaclases, lo que genera ambigüedad.

Casos de uso de metaclases

En la mayoría de los casos, no necesitamos una metaclase, el código normal encajará con la clase y el objeto. El uso inútil de metaclases aumenta la complejidad de la codificación. Pero hay escenarios donde la metaclase proporciona soluciones claras y eficientes. Veamos algunos casos de uso. 

Verificación de clase

Si necesita diseñar una clase que acepte una interfaz en particular, una metaclase es la solución adecuada. Podemos considerar un código de muestra donde una clase requiere que se establezca cualquiera de los atributos. Repasemos el código.

Python3

class MainClass(type):
    def __new__(cls, name, bases, attrs):
        if 'foo' in attrs and 'bar' in attrs:
            raise TypeError('Class % s cannot contain both foo and bar \
attributes.' % name)
        if 'foo' not in attrs and 'bar' not in attrs:
            raise TypeError('Class % s must provide either a foo \
attribute or a bar attribute.' % name)
        else:
          print('Success')
              
  
        return super(MainClass, cls).__new__(cls, name, bases, attrs)
  
class SubClass(metaclass = MainClass):
    foo = 42
    bar = 34
  
  
subCls = SubClass()

Aquí tratamos de establecer dos atributos. Por lo tanto, el diseño impidió que se configurara y generó el siguiente error. 

Traceback (most recent call last):
  File "/home/fe76a380911f384c4517f07a8de312a4.py", line 13, in <module>
    class SubClass(metaclass = MainClass):
  File "/home/fe76a380911f384c4517f07a8de312a4.py", line 5, in __new__
    attributes.' %name)
TypeError: Class SubClass cannot both foo and bar attributes.


Puede pensar que, al usar decoradores, puede crear fácilmente una clase que esté de acuerdo con un estándar en particular. Pero la desventaja de usar el decorador de clases es que debe aplicarse explícitamente a cada subclase. 

Impedir heredar los atributos

Una metaclase es una herramienta eficiente para evitar que una subclase herede ciertas funciones de clase. Este escenario se puede explicar mejor en el caso de las clases abstractas. Al crear clases abstractas, no es necesario ejecutar la funcionalidad de la clase. Echemos un vistazo al siguiente código.  

Python3

class MetaCls(type):
    def __new__(cls, name, bases, attrs):
        # If abstract class, then skip the metaclass function
        if attrs.pop('abstract', False):
            print('Abstract Class:', name)
            return super(MetaCls, cls).__new__(cls, name, bases, attrs)
          
        # metaclass functionality
        if 'foo' in attrs and 'bar' in attrs:
            raise TypeError('Class % s cannot contain both foo and bar \
attributes.' % name)
        if 'foo' not in attrs and 'bar' not in attrs:
            raise TypeError('Class % s must provide either a foo \
attribute or a bar attribute.' % name)
        print('Normal Class:', name)
        return super(MetaCls, cls).__new__(cls, name, bases, attrs)
  
class AbsCls(metaclass = MetaCls):
    abstract = True
      
  
class NormCls(metaclass = MetaCls):
    foo = 42
Producción

Abstract Class: AbsCls
Normal Class: NormCls



Generación dinámica de clases

La generación dinámica de clases abre muchas posibilidades. Veamos cómo generar clases dinámicamente usando type.

Python3

class FoodType(object):
    events = []
  
    def __init__(self, ftype, items):
        self.ftype = ftype
        self.items = items
        FoodType.events.append(self)
  
    def run(self):
        print("Food Type: % s" %(self.ftype))
        print("Food Menu:", self.items) 
  
    @staticmethod
    def run_events():
        for e in FoodType.events:
            e.run()
  
def sub_food(ftype):
    class_name = ftype.capitalize()
    def __init__(self, items):
        FoodType.__init__(self, ftype, items)
    # dynamic class creation and defining it as a global attribute    
    globals()[class_name] = \
    type(class_name, (FoodType, ), dict(__init__ = __init__))
            
          
if __name__ == "__main__":
    foodType = ["Vegetarian", "Nonvegetarian"]
    foodItems = "Vegetarian(['Spinach', 'Bitter Guard']);\
Nonvegetarian(['Meat', 'Fish'])"
    # invoking method for dynamic class creation.
    [sub_food(ftype) for ftype in foodType]
    # executing dynamic classes.
    exec(foodItems)
    FoodType.run_events()
Producción

Food Type: Vegetarian
Food Menu: ['Spinach', 'Bitter Guard']
Food Type: Nonvegetarian
Food Menu: ['Meat', 'Fish']



En este caso, creamos dos subclases, vegetariana y no vegetariana, dinámicamente, que hereda de la clase FoodType.

Resumen

Las clases normales que están diseñadas usando la palabra clave class tienen el tipo como sus metaclases, y el tipo es la metaclase principal. Las metaclases son una herramienta poderosa en Python que puede superar muchas limitaciones. Pero la mayoría de los desarrolladores tienen la idea errónea de que las metaclases son difíciles de entender.

Publicación traducida automáticamente

Artículo escrito por SonuGeorge y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *