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 técnica, llamada metaprogramación, es popular entre los desarrolladores de marcos de código. Así es como obtienes 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. Este tipo de clase, en resumen, contiene las instrucciones sobre la generación de código detrás de escena que desea que tenga lugar cuando se está ejecutando otra pieza de código.

    Wikipedia resume bastante bien las metaclases:

    En la 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 plano de la clase en sí?

    Aquí es donde entra una metaclase. Una metaclase es el modelo de la clase en sí, al igual que una clase es el modelo de 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 están definidas en nuestro código.

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

    # 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 el nuevo TryHello la clase puede imprimir un saludo que dice:

    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 es HelloMeta en este caso, genera el código en tiempo de ejecución que agrega automáticamente el método a la clase.

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

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

    Las metaclases nos dan la capacidad de escribir código que transforma, no solo datos, sino otro código, por ejemplo, transformando una clase en el momento en que se crea una instancia. En el ejemplo anterior, nuestra metaclase agrega un nuevo método automáticamente 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 alguna forma de transformación de código en segundo plano.

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

    Piense en los marcos de software que generan 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) y Aumentar(C ++) son ejemplos de dónde los autores de marcos utilizan la metaprogramación para generar código y encargarse de 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.

    Encargarse de hacer que la simplicidad funcione entre bastidores es una gran cantidad de metaprogramación incorporada 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 o objetos de 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:

    >>> 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 esa variable day es de tipo str, que es un tipo de cadena. Puede encontrar el tipo de cualquier objeto simplemente usando el type función con un argumento de objeto.

    Encontrar el tipo de clase

    Entonces, una cadena como "Sunday" o "hello" es de tipo str, pero que pasa str ¿sí mismo? ¿Cuál es el tipo de str ¿clase?

    Nuevamente, escriba en la consola de Python:

    >>> type(str)
    <type 'type'>
    

    Esta vez, obtenemos una impresión que str es de tipo type.

    Tipo y el tipo de tipo

    Pero que pasa type ¿sí mismo? Que es typetipo?

    >>> type(type)
    <type 'type'>
    

    El resultado es, una vez más, «tipo». Así encontramos que type no es solo la metaclase de clases como int, ¡también es su propia metaclase!

    Métodos especiales utilizados por las 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 solo objetos simples.

    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.

    los type 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__: Este método se llama para configurar valores después de que se crea 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 type, que es la metaclase predeterminada.

    Práctica de metaprogramación 1: uso de decoradores para transformar el comportamiento de las funciones

    Demos un paso atrás antes de continuar 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:

    # 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:

    >>> 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. Debería utilizar decoradores donde necesite aplicar 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 decoradora

    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 crea el nuevo objeto. El resultado es una mayor flexibilidad en cuanto a para qué puede utilizarlos.

    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 junto con 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.

    # 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!
    

    Clases que usan nuestro Notifies metaclase, por ejemplo Shouter y Math, sus métodos se reemplazan, en el momento de la creación, con versiones mejoradas que primero nos notifican a través de un print declaración del nombre del método que se está ejecutando. Esto es idéntico al comportamiento que implementamos antes de usar una función decoradora.

    Ejemplo 1 de metaclases: implementación de una clase que no puede ser subclasificada

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

    Por ejemplo, singletons se utilizan en muchas bibliotecas de códigos. 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 última clase es otro ejemplo de control del uso de clases. Con una clase final, la clase no permite que se creen subclases. Las clases finales se utilizan en algunos marcos por 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.

    # 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 un Final clase. 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 nuestro código base.

    Ejemplo 2 de metaclases: Creación de un tiempo de ejecución de la operación de seguimiento de clases

    Perfiladores se utilizan para hacer un balance del uso de recursos en un sistema informático. Un generador de perfiles puede rastrear cosas como el uso de 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 del generador de perfiles.

    # 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 un Timed metaclase que reescribe sus clases sobre la marcha. Siempre que una nueva clase que usa el Timed se declara metaclase, sus métodos se reescriben para ser cronometrados por nuestra clase de utilidad de temporizador. Siempre que ejecutamos cálculos usando un Timed class, hacemos el cronometraje automáticamente, sin necesidad de hacer nada adicional.

    La metaprogramación es una gran herramienta si está escribiendo código y herramientas para ser utilizadas por otros desarrolladores, como frameworks web o depuradores. Con la generación de código y la metaprogramación, puede facilitarles la vida a los programadores que hacen uso de sus bibliotecas de códigos.

     

    Dominando 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 el oficial Metaclases de Python 3 documentación.

    Etiquetas:

    Deja una respuesta

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