Métodos lazy (opcionales) en una clase

Posted on Fri 13 April 2018 in Python

Este post es un apunte rápido para documentar una implementación de funciones lazy a través de decoradores en una clase de Ptyhon. La idea es tener una forma sencilla de definir funciones de una clase que:

  • se puedan ejecutar tan pronto como se crea un objeto (o no),
  • almacenen en caché el resultado de la función

esta implementación puede ser útil en el contexto de clases cuyo objetivo es parsear archivos de texto pesados. La ejecución inmediata permita conocer tan pronto como sea posible si hay algún error en el formato.

Las piezas necesarias para montarlo son:

  • Un decorador:

    class lazy(object):
    
        def __init__(self, func):
            self.func = func
            self.lazy_keyword = '__{}'.format(func.__name__)
    
        def __call__(self, *args, **kwargs):
            log.warning("Do not use @lazy for free functions like '{}'. Did you intend @memoize?".format(self.func.__name__))
            return self.func(*args, **kwargs)
    
        def __get__(self, inst, cls=None):
            if inst is not None:
                def wrapped_func(*args, **kwargs):
                    if not hasattr(inst, self.lazy_keyword):
                        # setattr(inst, self.lazy_keyword, self.func(inst, *args, **kwargs))
                        setattr(inst, self.lazy_keyword, types.MethodType(self.func, inst, cls)(*args, **kwargs))
                    return getattr(inst, self.lazy_keyword)
                return wrapped_func
            else:
                return self
    
        def init_cls(self, name, cls):
            assert issubclass(cls, LazyMixin), "Use @lazy for member functions of classes that inherit from LazyMixin"
    
            def wrapped_func(inst, *args, **kwargs):
                if not hasattr(inst, self.lazy_keyword):
                    # setattr(inst, self.lazy_keyword, self.func(inst, *args, **kwargs))
                    setattr(inst, self.lazy_keyword, types.MethodType(self.func, inst, cls)(*args, **kwargs))
                return getattr(inst, self.lazy_keyword)
    
            cls._lazy_functions.append(wrapped_func)
    
  • Una metaclase:

    class MetaLazy(type):
    
        def __init__(cls, name, bases, classdict):
            if name != 'LazyMixin':
                cls._lazy_functions = []
    
            # getmembers retrieves all attributes
            # including those inherited from parents
            for k, v in inspect.getmembers(cls):
                if isinstance(v, lazy):
                    v.init_cls(k, cls)
    
  • Una clase base:

    class LazyMixin(object):
        __metaclass__ = MetaLazy
    
        _lazy_functions = []
    
        def invoke_lazy_functions(self):
            for func in self._lazy_functions:
                func(self)
    

Apoyándose en esto, uno puede definir clases que tengan funciones lazy:

class ALazyClass(LazyMixin):

    def __init__(self, lazy=True, *args, **kwargs):
        if not lazy:
            self.invoke_lazy_functions()

    @lazy
    def lazy1(self):
        print("return 23")
        return 23

...espero volver pronto por aquí para mejorar este apunte.