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 *