Metaclases y metaprogramación de Python

Imagínese si pudiera tener programas de computadora que escribieran su código por usted. Es posible, ¡pero las máquinas no escribirán todo su código por usted! Esta tecnica...

Imagínese si pudiera tener programas de computadora que escribieran su código por usted. Es posible, ¡pero las máquinas no escribirán todo su código por usted!

Esta técnica, llamada metaprogramación, es popular entre los desarrolladores de frameworks de código. Así es como obtiene la generación de código y funciones inteligentes en muchos marcos y bibliotecas populares como Ruby On Rails o TensorFlow.

Los lenguajes de programación funcional como Elixir, Clojure y Ruby se destacan por sus capacidades de metaprogramación. En esta guía, le mostramos cómo puede aprovechar el poder de la metaprogramación en Python. Los ejemplos de código están escritos para Python 3, pero funcionarán para Python 2 con algunos ajustes.

¿Qué es una metaclase en Python?

Python es un lenguaje orientado a objetos que facilita el trabajo con clases.

La metaprogramación en Python se basa en un nuevo tipo especial de clase que se llama metaclase. En resumen, este tipo de clase contiene las instrucciones sobre la generación de código detrás de escena que desea que tenga lugar cuando se ejecuta otra pieza de código.

Wikipedia resume bastante bien las metaclases:

En programación orientada a objetos, una metaclase es una clase cuyas instancias son clases

Cuando definimos una clase, los objetos de esa clase se crean utilizando la clase como modelo.

Pero, ¿qué pasa con la clase en sí? ¿Cuál es el modelo de la clase en sí?

Aquí es donde entra en juego una metaclase. Una metaclase es el modelo de la clase en sí, al igual que una clase es el modelo para las instancias de esa clase. Una metaclase es una clase que define propiedades de otras clases.

Con una metaclase, podemos definir propiedades que deben agregarse a las nuevas clases que se definen en nuestro código.

Por ejemplo, el siguiente ejemplo de código de metaclase agrega una propiedad hola a cada clase que usa esta metaclase como plantilla. Esto significa que las nuevas clases que son instancias de esta metaclase tendrán una propiedad hola sin necesidad de definirla ellos mismos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# hello_metaclass.py
# A simple metaclass
# This metaclass adds a 'hello' method to classes that use the metaclass
# meaning, those classes get a 'hello' method with no extra effort
# the metaclass takes care of the code generation for us
class HelloMeta(type):
    # A hello method
    def hello(cls):
        print("greetings from %s, a HelloMeta type class" % (type(cls())))

    # Call the metaclass
    def __call__(self, *args, **kwargs):
        # create the new class as normal
        cls = type.__call__(self, *args)

        # define a new hello method for each of these classes
        setattr(cls, "hello", self.hello)

        # return the class
        return cls

# Try out the metaclass
class TryHello(object, metaclass=HelloMeta):
    def greet(self):
        self.hello()

# Create an instance of the metaclass. It should automatically have a hello method
# even though one is not defined manually in the class
# in other words, it is added for us by the metaclass
greeter = TryHello()
greeter.greet()

El resultado de ejecutar este código es que la nueva clase TryHello puede imprimir un saludo que dice:

1
greetings from <class '__main__.TryHello'>, a HelloMeta type class

El método responsable de esta impresión no está declarado en la declaración de la clase. Más bien, la metaclase, que en este caso es HelloMeta, genera el código en tiempo de ejecución que automáticamente agrega el método a la clase.

Para verlo en acción, no dude en copiar y pegar el código en una consola de Python. Además, lea los comentarios para comprender mejor lo que hemos hecho en cada parte del código. Tenemos un nuevo objeto, llamado saludo, que es una instancia de la clase TryHello. Sin embargo, podemos llamar al método self.hello de TryHello aunque no se haya definido tal método en la declaración de la clase TryHello.

En lugar de recibir un error por llamar a un método que no existe, TryHello obtiene dicho método automáticamente debido al uso de la clase HelloMeta como su metaclase.

Las metaclases nos brindan la capacidad de escribir código que transforma, no solo datos, sino otro código, p. transformando una clase en el momento en que se instancia. En el ejemplo anterior, nuestra metaclase agrega automáticamente un nuevo método a las nuevas clases que definimos para usar nuestra metaclase como su metaclase.

Este es un ejemplo de metaprogramación. La metaprogramación es simplemente escribir código que funciona con metaclases y técnicas relacionadas para realizar algún tipo de transformación de código en segundo plano.

Lo hermoso de la metaprogramación es que, en lugar de generar el código fuente, nos devuelve solo la ejecución de ese código. El usuario final de nuestro programa no se da cuenta de la "magia" que ocurre en segundo plano.

Piense en marcos de software que generen código en segundo plano para asegurarse de que usted, como programador, tenga que escribir menos código para todo. Aquí hay algunos buenos ejemplos:

Fuera de Python, otras bibliotecas populares como Ruby on Rails(Ruby) e Impulsar(C++) son ejemplos de donde la metaprogramación es utilizado por los autores de marcos para generar código y cuidar las cosas en segundo plano.

El resultado son API de usuario final simplificadas que automatizan una gran cantidad de trabajo para el programador que codifica en el marco.

Cuidar de hacer que esa simplicidad funcione detrás de escena, es una gran cantidad de metaprogramación integrada en el código fuente del marco.

Sección de teoría: comprender cómo funcionan las metaclases

Para comprender cómo funcionan las metaclases de Python, debe sentirse muy cómodo con la noción de tipos en Python.

Un tipo es simplemente la nomenclatura de datos u objetos para un objeto en Python.

Encontrar el tipo de un objeto

Usando Python REPL, creemos un objeto de cadena simple e inspeccionemos su tipo, de la siguiente manera:

1
2
3
>>> day = "Sunday"
>>> print("The type of variable day is %s" % (type(day)))
The type of variable day is <type 'str'>

Como era de esperar, obtenemos una impresión de que la variable día es del tipo str, que es un tipo de cadena. Puede encontrar el tipo de cualquier objeto simplemente usando la función tipo incorporada con un argumento de objeto.

Encontrar el tipo de una clase

Entonces, una cadena como "Domingo" o "hola" es del tipo str, pero ¿qué pasa con str en sí misma? ¿Cuál es el tipo de la clase str?

Nuevamente, escriba en la consola de Python:

1
2
>>> type(str)
<type 'type'>

Esta vez, obtenemos una impresión de que ‘str’ es del tipo ’tipo’.

Tipo y el tipo de tipo

Pero, ¿qué pasa con el tipo en sí mismo? ¿Qué es el tipo type's?

1
2
>>> type(type)
<type 'type'>

El resultado es, una vez más, "tipo". Por lo tanto, encontramos que type no es solo la metaclase de clases como int, ¡también es su propia metaclase!

Métodos especiales usados ​​por metaclases

En este punto puede ser útil revisar un poco la teoría. Recuerde que una metaclase es una clase cuyas instancias son en sí mismas clases y no simples objetos.

En Python 3, puede asignar una metaclase a la creación de una nueva clase pasando la clase magistral prevista a la nueva definición de clase.

El tipo type, como la metaclase predeterminada en Python, define métodos especiales que las nuevas metaclases pueden anular para implementar un comportamiento de generación de código único. Aquí hay una breve descripción de estos métodos "mágicos" que existen en una metaclase:

  • __new__: este método se llama en la metaclase antes de que se cree una instancia de una clase basada en la metaclase
  • __init__: se llama a este método para configurar valores después de crear la instancia/objeto
  • __prepare__: define el espacio de nombres de la clase en una asignación que almacena los atributos
  • __call__: este método se llama cuando el constructor de la nueva clase se va a utilizar para crear un objeto

Estos son los métodos para anular en su metaclase personalizada para dar a sus clases un comportamiento diferente al de tipo, que es la metaclase predeterminada.

Práctica de metaprogramación 1: Uso de decoradores para transformar el comportamiento de las funciones {#Práctica de metaprogramación 1 mediante el uso de decoradores para transformar el comportamiento de las funciones}

Demos un paso atrás antes de proceder con el uso de la práctica de metaprogramación de metaclases. Un uso común de la metaprogramación en Python es el uso de decoradores.

Un decorador es una función que transforma la ejecución de una función. En otras palabras, toma una función como entrada y devuelve otra función.

Por ejemplo, aquí hay un decorador que toma cualquier función e imprime el nombre de la función antes de ejecutar la función original normalmente. Esto podría ser útil para registrar llamadas a funciones, por ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# decorators.py

from functools import wraps

# Create a new decorator named notifyfunc
def notifyfunc(fn):
    """prints out the function name before executing it"""
    @wraps(fn)
    def composite(*args, **kwargs):
        print("Executing '%s'" % fn.__name__)
        # Run the original function and return the result, if any
        rt = fn(*args, **kwargs)
        return rt
    # Return our composite function
    return composite

# Apply our decorator to a normal function that prints out the result of multiplying its arguments
@notifyfunc
def multiply(a, b):
    product = a * b
    return product

Puede copiar y pegar el código en un REPL de Python. Lo bueno de usar el decorador es que la función compuesta se ejecuta en lugar de la función de entrada. El resultado del código anterior es que la función de multiplicación anuncia que se está ejecutando antes de que se ejecute su cálculo:

1
2
3
4
5
6
7
>>> multiply(5, 6)
Executing 'multiply'
30
>>>
>>> multiply(89, 5)
Executing 'multiply'
445

En resumen, los decoradores logran el mismo comportamiento de transformación de código de las metaclases, pero son mucho más simples. Le gustaría usar decoradores donde necesite aplicar una metaprogramación común alrededor de su código. Por ejemplo, podría escribir un decorador que registre todas las llamadas a la base de datos.

Práctica de metaprogramación 2: Uso de metaclases como una función de decorador {#Práctica de metaprogramación 2utilizando metaclases como una función de decorador}

Las metaclases pueden reemplazar o modificar atributos de clases. Tienen el poder de conectarse antes de que se cree un nuevo objeto o después de que se cree el nuevo objeto. El resultado es una mayor flexibilidad con respecto a para qué puede usarlos.

A continuación, creamos una metaclase que logra el mismo resultado que el decorador del ejemplo anterior.

Para comparar los dos, debe ejecutar ambos ejemplos uno al lado del otro y luego seguir el código fuente anotado. Tenga en cuenta que puede copiar el código y pegarlo directamente en su REPL, si su REPL conserva el formato del código.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# metaclassdecorator.py
import types

# Function that prints the name of a passed in function, and returns a new function
# encapsulating the behavior of the original function
def notify(fn, *args, **kwargs):

    def fncomposite(*args, **kwargs):
        # Normal notify functionality
        print("running %s" % fn.__name__)
        rt = fn(*args, **kwargs)
        return rt
    # Return the composite function
    return fncomposite

# A metaclass that replaces methods of its classes
# with new methods 'enhanced' by the behavior of the composite function transformer
class Notifies(type):

    def __new__(cls, name, bases, attr):
        # Replace each function with
        # a print statement of the function name
        # followed by running the computation with the provided args and returning the computation result
        for name, value in attr.items():
            if type(value) is types.FunctionType or type(value) is types.MethodType:
                attr[name] = notify(value)

        return super(Notifies, cls).__new__(cls, name, bases, attr)

# Test the metaclass
class Math(metaclass=Notifies):
    def multiply(a, b):
        product = a * b
        print(product)
        return product

Math.multiply(5, 6)

# Running multiply():
# 30


class Shouter(metaclass=Notifies):
    def intro(self):
        print("I shout!")

s = Shouter()
s.intro()

# Running intro():
# I shout!

Las clases que usan nuestra metaclase Notify, por ejemplo, Shouter y Math, tienen sus métodos reemplazados, en el momento de la creación, con versiones mejoradas que primero nos notifican a través de una declaración print del nombre del método que se está ejecutando. Esto es idéntico al comportamiento que implementamos antes de usar una función de decorador.

Metaclases Ejemplo 1: Implementación de una clase que no se puede subclasificar {#metaclasesejemplo1implementación de una clase que no se puede subclasificar}

Los casos de uso comunes para la metaprogramación incluyen instancias de clases de control.

Por ejemplo, [solteros](https://www.packtpub.com/books/content/python-design-patterns- depth-singleton-pattern) se utilizan en muchas bibliotecas de código. Una clase singleton controla la creación de instancias de modo que solo haya como máximo una instancia de la clase en el programa.

Una clase final es otro ejemplo de uso de clases de control. Con una clase final, la clase no permite crear subclases. Las clases finales se utilizan en algunos marcos por motivos de seguridad, lo que garantiza que la clase conserve sus atributos originales.

A continuación, damos una implementación de una clase final usando una metaclase para restringir que la clase sea heredada por otra.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# final.py

# a final metaclass. Subclassing a class that has the Final metaclass should fail
class Final(type):
    def __new__(cls, name, bases, attr):
        # Final cannot be subclassed
        # check that a Final class has not been passed as a base
        # if so, raise error, else, create the new class with Final attributes
        type_arr = [type(x) for x in bases]
        for i in type_arr:
            if i is Final:
                raise RuntimeError("You cannot subclass a Final class")
        return super(Final, cls).__new__(cls, name, bases, attr)


# Test: use the metaclass to create a Cop class that is final

class Cop(metaclass=Final):
    def exit():
        print("Exiting...")
        quit()

# Attempt to subclass the Cop class, this should idealy raise an exception!
class FakeCop(Cop):
    def scam():
        print("This is a hold up!")

cop1 = Cop()
fakecop1 = FakeCop()

# More tests, another Final class
class Goat(metaclass=Final):
    location = "Goatland"

# Subclassing a final class should fail
class BillyGoat(Goat):
    location = "Billyland"

En el código, hemos incluido declaraciones de clase para intentar subclasificar una clase Final. Estas declaraciones fallan, lo que genera excepciones. El uso de una metaclase que restringe la subclasificación de sus clases nos permite implementar clases finales en nuestra base de código.

Ejemplo 2 de metaclases: crear un tiempo de ejecución de operación de seguimiento de clase

Perfiladores se utilizan para evaluar el uso de recursos en un sistema informático. Un generador de perfiles puede rastrear cosas como el uso de la memoria, la velocidad de procesamiento y otras métricas técnicas.

Podemos usar una metaclase para realizar un seguimiento del tiempo de ejecución del código. Nuestro ejemplo de código no es un generador de perfiles completo, pero es una prueba de concepto de cómo puede realizar la metaprogramación para una funcionalidad similar a la de un generador de perfiles.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
# timermetaclass.py
import types

# A timer utility class
import time

class Timer:
    def __init__(self, func=time.perf_counter):
        self.elapsed = 0.0
        self._func = func
        self._start = None

    def start(self):
        if self._start is not None:
            raise RuntimeError('Already started')
        self._start = self._func()

    def stop(self):
        if self._start is None:
            raise RuntimeError('Not started')
        end = self._func()
        self.elapsed += end - self._start
        self._start = None

    def reset(self):
        self.elapsed = 0.0

    @property
    def running(self):
        return self._start is not None

    def __enter__(self):
        self.start()
        return self

    def __exit__(self, *args):
        self.stop()


# Below, we create the Timed metaclass that times its classes' methods
# along with the setup functions that rewrite the class methods at
# class creation times


# Function that times execution of a passed in function, returns a new function
# encapsulating the behavior of the original function
def timefunc(fn, *args, **kwargs):

    def fncomposite(*args, **kwargs):
        timer = Timer()
        timer.start()
        rt = fn(*args, **kwargs)
        timer.stop()
        print("Executing %s took %s seconds." % (fn.__name__, timer.elapsed))
        return rt
    # return the composite function
    return fncomposite

# The 'Timed' metaclass that replaces methods of its classes
# with new methods 'timed' by the behavior of the composite function transformer
class Timed(type):

    def __new__(cls, name, bases, attr):
        # replace each function with
        # a new function that is timed
        # run the computation with the provided args and return the computation result
        for name, value in attr.items():
            if type(value) is types.FunctionType or type(value) is types.MethodType:
                attr[name] = timefunc(value)

        return super(Timed, cls).__new__(cls, name, bases, attr)

# The below code example test the metaclass
# Classes that use the Timed metaclass should be timed for us automatically
# check the result in the REPL

class Math(metaclass=Timed):

    def multiply(a, b):
        product = a * b
        print(product)
        return product

Math.multiply(5, 6)


class Shouter(metaclass=Timed):

    def intro(self):
        print("I shout!")

s = Shouter()
s.intro()


def divide(a, b):
    result = a / b
    print(result)
    return result

div = timefunc(divide)
div(9, 3)

Como puede ver, pudimos crear una metaclase ’temporizada’ que reescribe sus clases sobre la marcha. Cada vez que se declara una nueva clase que usa la metaclase Timed, sus métodos se reescriben para que sean cronometrados por nuestra clase de utilidad de temporizador. Cada vez que ejecutamos cálculos utilizando una clase Timed, obtenemos el tiempo automáticamente, sin necesidad de hacer nada adicional.

La metaprogramación es una gran herramienta si está escribiendo código y herramientas para que las utilicen otros desarrolladores, como marcos web o depuradores. Con la generación de código y la metaprogramación, puede facilitar la vida de los programadores que utilizan sus bibliotecas de código.

Curso sugerido: Dominar Python

Dominar el poder de las metaclases {#dominar el poder de las metaclases}

Las metaclases y la metaprogramación tienen mucho poder. La desventaja es que la metaprogramación puede volverse bastante complicada. En muchos casos, el uso de decoradores proporciona una forma más sencilla de obtener una solución elegante. Las metaclases deben usarse cuando las circunstancias exigen generalidad en lugar de simplicidad.

Para hacer un uso efectivo de las metaclases, sugerimos leyendo en las Metaclases de Python 3 oficiales dev/peps/pep-3115/) documentación.

Licensed under CC BY-NC-SA 4.0