Seguimos hablando de metaprogramación en Python. Cuando se usa correctamente, le permite implementar rápida y elegantemente patrones de diseño complejos. En la última parte de este artículo, mostramos cómo se pueden usar las metaclases para cambiar los atributos de instancias y clases.

Ahora veamos cómo puede cambiar las llamadas a métodos. Puede obtener más información sobre las opciones de metaprogramación en el curso avanzado de Python .
Depuración y rastreo de llamadas
Como ya entendió, con la ayuda de una metaclase, cualquier clase puede transformarse más allá del reconocimiento. Por ejemplo, reemplace todos los métodos de clase con otros o aplique un decorador arbitrario a cada método. Puede usar esta idea para depurar el rendimiento de la aplicación.
La siguiente metaclase mide el tiempo de ejecución de cada método en la clase y sus instancias, así como el tiempo de creación de la propia instancia:
from contextlib import contextmanager import logging import time import wrapt @contextmanager def timing_context(operation_name): """ """ start_time = time.time() try: yield finally: logging.info('Operation "%s" completed in %0.2f seconds', operation_name, time.time() - start_time) @wrapt.decorator def timing(func, instance, args, kwargs): """ . https://wrapt.readthedocs.io/en/latest/ """ with timing_context(func.__name__): return func(*args, **kwargs) class DebugMeta(type): def __new__(mcs, name, bases, attrs): for attr, method in attrs.items(): if not attr.startswith('_'):
Veamos la depuración en acción:
class User(metaclass=DebugMeta): def __init__(self, name): self.name = name time.sleep(.7) def login(self): time.sleep(1) def logout(self): time.sleep(2) @classmethod def create(cls): time.sleep(.5) user = User('Michael') user.login() user.logout() user.create()
Intente expandir DebugMeta
y registre la firma de los métodos y su seguimiento de pila.
El patrón solitario y la prohibición de la herencia.
Y ahora pasemos a casos exóticos de uso de metaclases en proyectos de Python.
Seguramente, muchos de ustedes usan el módulo Python habitual para implementar un patrón de diseño singleton (también conocido como Singleton), porque es mucho más conveniente y más rápido que escribir la metaclase apropiada. Sin embargo, escribamos una de sus implementaciones en aras del interés académico:
class Singleton(type): instance = None def __call__(cls, *args, **kwargs): if cls.instance is None: cls.instance = super().__call__(*args, **kwargs) return cls.instance class User(metaclass=Singleton): def __init__(self, name): self.name = name def __repr__(self): return f'<User: {self.name}>' u1 = User('Pavel')
Esta implementación tiene un matiz interesante: dado que el constructor de la clase no se llama por segunda vez, puede cometer un error y no pasar el parámetro necesario allí, y nada sucederá en tiempo de ejecución si la instancia ya se ha creado. Por ejemplo:
>>> User('Roman') <User: Roman> >>> User('Alexey', 'Petrovich', 66)
Ahora echemos un vistazo a una opción aún más exótica: la prohibición de la herencia de una clase en particular.
class FinalMeta(type): def __new__(mcs, name, bases, attrs): for cls in bases: if isinstance(cls, FinalMeta): raise TypeError(f"Can't inherit {name} class from final {cls.__name__}") return super().__new__(mcs, name, bases, attrs) class A(metaclass=FinalMeta): """ !""" pass class B(A): pass
En los ejemplos anteriores, utilizamos metaclases para personalizar la creación de clases, pero puede ir aún más lejos y comenzar a parametrizar el comportamiento de las metaclases.
Por ejemplo, puede pasar una función al parámetro de metaclase al declarar una clase y devolver diferentes instancias de metaclases dependiendo de algunas condiciones, por ejemplo:
def get_meta(name, bases, attrs): if SOME_SETTING: return MetaClass1(name, bases, attrs) else: return MetaClass2(name, bases, attrs) class A(metaclass=get_meta): pass
Pero un ejemplo más interesante es el uso de parámetros extra_kwargs
al declarar clases. Suponga que desea usar la metaclase para cambiar el comportamiento de ciertos métodos en una clase, y cada clase puede tener diferentes nombres para estos métodos. Que hacer Y esto es lo que
En mi opinión, resultó muy elegante! Puede crear muchos patrones para usar esta parametrización, pero recuerde la regla principal: todo es bueno con moderación.
__prepare__
métodos
Finalmente, hablaré sobre el posible uso del método __prepare__
. Como se mencionó anteriormente, este método debe devolver un objeto de diccionario, que el intérprete llena en el momento de analizar el cuerpo de la clase, por ejemplo, si __prepare__
devuelve el objeto d = dict()
, cuando lee la siguiente clase:
class A: x = 12 y = 'abc' z = {1: 2}
El intérprete realizará las siguientes operaciones:
d['x'] = 12 d['y'] = 'abc' d['z'] = {1: 2}
Hay varios usos posibles para esta característica. Todos tienen diversos grados de utilidad, por lo tanto:
- En las versiones de Python = <3.5, si necesitáramos mantener el orden de los métodos de declaración en la clase, podríamos devolver
collections.OrderedDict
__prepare__
método __prepare__
, en versiones anteriores, los diccionarios integrados ya conservan el orden de agregar claves, por lo que OrderedDict
ya no es necesario. - El módulo de biblioteca estándar
enum
utiliza un objeto tipo dict personalizado para determinar cuándo se duplica un atributo de clase en la declaración. El código se puede encontrar aquí . - No es un código listo para producción en absoluto, pero un muy buen ejemplo es el soporte para polimorfismo paramétrico .
Por ejemplo, considere la siguiente clase con tres implementaciones de un único método polimórfico:
class Terminator: def terminate(self, x: int): print(f'Terminating INTEGER {x}') def terminate(self, x: str): print(f'Terminating STRING {x}') def terminate(self, x: dict): print(f'Terminating DICTIONARY {x}') t1000 = Terminator() t1000.terminate(10) t1000.terminate('Hello, world!') t1000.terminate({'hello': 'world'})
Obviamente, el último método de terminate
declarado sobrescribe las implementaciones de los dos primeros, y necesitamos que el método sea seleccionado dependiendo del tipo de argumento pasado. Para lograr esto, programamos un par de objetos de envoltura adicionales:
class PolyDict(dict): """ , PolyMethod. """ def __setitem__(self, key: str, func): if not key.startswith('_'): if key not in self: super().__setitem__(key, PolyMethod()) self[key].add_implementation(func) return None return super().__setitem__(key, func) class PolyMethod: """ , . , : instance method, staticmethod, classmethod. """ def __init__(self): self.implementations = {} self.instance = None self.cls = None def __get__(self, instance, cls): self.instance = instance self.cls = cls return self def _get_callable_func(self, impl):
Lo más interesante en el código anterior es el objeto PolyMethod
, que almacena un registro con implementaciones del mismo método, dependiendo del tipo de argumento pasado a este método. PolyDict
objeto __prepare__
método __prepare__
y, por lo tanto, __prepare__
diferentes implementaciones de los métodos con el mismo nombre terminate
. Un punto importante: al leer el cuerpo de la clase y al crear el objeto attrs
, el intérprete coloca allí las llamadas funciones no attrs
, estas funciones aún no saben en qué clase o instancia serán llamadas. Tuvimos que implementar un protocolo descriptor para definir el contexto durante la llamada a la función y pasar self
o cls
como primer parámetro, o no pasar nada si staticmethod
llama al staticmethod
.
Como resultado, veremos la siguiente magia:
class PolyMeta(type): @classmethod def __prepare__(mcs, name, bases): return PolyDict() class Terminator(metaclass=PolyMeta): ... t1000 = Terminator() t1000.terminate(10) t1000.terminate('Hello, world!') t1000.terminate({'hello': 'world'})
Si conoce otros usos interesantes del método __prepare__
, escriba en los comentarios.
Conclusión
La metaprogramación es uno de los muchos temas de los que hablé en Advanced Python . Como parte del curso, también le diré cómo usar de manera efectiva los principios de SOLID y GRASP en el desarrollo de grandes proyectos de Python, diseñar la arquitectura de la aplicación y escribir código de alto rendimiento y alta calidad. ¡Estaré encantado de verte en las paredes del Distrito Binario!