django源码分析之惰性加载(lazy和LazyObject)

惰性加载

惰性加载是一种延迟计算的技术,当只有真正需要使用结果的时候才会去计算。Django提供了两种惰性加载模块,分别是lazy和LazyObject,前者主要针对可以调用的对象,延迟函数的调用;后者针对类,延迟类的实例化。

代码分析

lazy模块
class Promise:
    """
    Base class for the proxy class created in the closure of the lazy function.
    It's used to recognize promises in code.
    """
    pass

类Promise作为那些在lazy函数内创建的惰性加载代理类的基类使用,用于识别代码中需要惰性加载的对象。

def lazy(func, *resultclasses):
    """
    Turn any callable into a lazy evaluated callable. result classes or types
    is required -- at least one is needed so that the automatic forcing of
    the lazy evaluation code is triggered. Results are not memoized; the
    function is evaluated on every access.
    """

    @total_ordering
    class __proxy__(Promise):
        """
        Encapsulate a function call and act as a proxy for methods that are
        called on the result of that function. The function is not evaluated
        until one of the methods on the result is called.
        """
        __prepared = False

        def __init__(self, args, kw):
            # 存储相关的参数,等实际调用函数的时候需要使用
            self.__args = args
            self.__kw = kw
            if not self.__prepared:
                # 第一次调用惰性函数时,会进入该分支,对代理类预先做一些准备工作
                self.__prepare_class__()
            self.__prepared = True

        def __reduce__(self):
            return (
                _lazy_proxy_unpickle,
                (func, self.__args, self.__kw) + resultclasses
            )

        def __repr__(self):
            return repr(self.__cast())

        @classmethod
        def __prepare_class__(cls):
            # 遍历所有结果类
            for resultclass in resultclasses:
                # 遍历结果类以及其基类
                for type_ in resultclass.mro():
                    # 遍历每个类的属性
                    for method_name in type_.__dict__:
                        # All __promise__ return the same wrapper method, they
                        # look up the correct implementation when called.
                        # 如果__proxy__已经有了该属性,则跳过
                        if hasattr(cls, method_name):
                            continue
                        # 针对结果类的属性,封装生成新的方法(增加了函数调用的逻辑),也就是在我们用结果类的该属性后,就会触发真正函数的调用
                        meth = cls.__promise__(method_name)
                        # 为代理类__proxy__增加该该属性,并绑定封装后的新方法
                        setattr(cls, method_name, meth)
            cls._delegate_bytes = bytes in resultclasses
            cls._delegate_text = str in resultclasses
            assert not (cls._delegate_bytes and cls._delegate_text), (
                "Cannot call lazy() with both bytes and text return types.")
            if cls._delegate_text:
                cls.__str__ = cls.__text_cast
            elif cls._delegate_bytes:
                cls.__bytes__ = cls.__bytes_cast

        @classmethod
        def __promise__(cls, method_name):
            # Builds a wrapper around some magic method
            def __wrapper__(self, *args, **kw):
                # Automatically triggers the evaluation of a lazy value and
                # applies the given magic method of the result type.
                # 这里增加了实际调用函数的逻辑。也就是用结果类的相关属性后,就会触发函数的调用。
                res = func(*self.__args, **self.__kw)
                return getattr(res, method_name)(*args, **kw)
            return __wrapper__

        def __text_cast(self):
            return func(*self.__args, **self.__kw)

        def __bytes_cast(self):
            return bytes(func(*self.__args, **self.__kw))

        def __bytes_cast_encoded(self):
            return func(*self.__args, **self.__kw).encode()

        def __cast(self):
            if self._delegate_bytes:
                return self.__bytes_cast()
            elif self._delegate_text:
                return self.__text_cast()
            else:
                return func(*self.__args, **self.__kw)

        def __str__(self):
            # object defines __str__(), so __prepare_class__() won't overload
            # a __str__() method from the proxied class.
            return str(self.__cast())

        def __eq__(self, other):
            if isinstance(other, Promise):
                other = other.__cast()
            return self.__cast() == other

        def __lt__(self, other):
            if isinstance(other, Promise):
                other = other.__cast()
            return self.__cast() < other

        def __hash__(self):
            return hash(self.__cast())

        def __mod__(self, rhs):
            if self._delegate_text:
                return str(self) % rhs
            return self.__cast() % rhs

        def __deepcopy__(self, memo):
            # Instances of this class are effectively immutable. It's just a
            # collection of functions. So we don't need to do anything
            # complicated for copying.
            memo[id(self)] = self
            return self

通过上面这段代码,可以看到代理类__proxy__主要做了下面两件事情:

1.保存需要延迟计算的函数的所有参数,等实际调用的时候需要使用;

2.在第一次调用函数的时候,根据结果类及其基类的属性去设置该代理类的属性,为这些属性绑定新的包装方法,该方法会调用被延迟计算的函数。

第2点是关键的地方,因为为代理类增加了结果类包含的属性,而且属性对应的方法里面会调用实际的函数,所以才能在使用结果的时候(这时候会触发结果类相关的属性),才会自动触发实际函数的调用,获取到真正的结果。达到了延迟函数调用的效果。

    @wraps(func)
    def __wrapper__(*args, **kw):
        # Creates the proxy object, instead of the actual value.
        return __proxy__(args, kw)

    return __wrapper__

这里需要说明的直接调用lazy()方法只是返回一个代理对象,而不是直接返回调用的结果。当实际使用结果的时候,该代理对象才会触发去调用相关的函数,返回计算结果。
下面用个简单的例子。

# 定一个需要惰性计算的函数,该函数只是简单返回字符串的title
def lazy_func(text):  
    return text.title()

# 获得一个惰性代理对象lazy_wraper
lazy_wraper = lazy(lazy_func, str)
# 注意,此时虽然调用了惰性函数,但并不会直接去调用lazy_func函数
res = lazy_wraper('hello world')
# 在对结果进行打印的时候,才会去实际调用lazy_func。
print(res)  

这个例子比较简单:

1.一开始调用lazy函数只是生成一个代理对象lazy_wraper

2.接着调用该对象的时候lazy_wraper('hello world'),因为是第一次调用,所以代理对象会进入__prepare_class__()方法,根据str类以及其基类的属性([<type 'str'>, <type 'basestring'>, <type 'object'>]),为代理类做相关的准备工作,主要是包装这些属性相关的方法,会添加实际调用lazy_func()的代码到与这些属性绑定的方法里面。

3.在调用print函数的时候,因为它会触发str的__str__方法,接着会接着触发代理对象去调用lazy_func函数得到结果,大致路径print(res) -> lazy_wraper.__str__() -> lazy_wraper.__text_cast() -> func(*self.__args, **self.__kw), 最后的func就是lazy_func('hello world')

其实每次触发都会重新调用函数来重新计算,所以如果需要的话可以改下相关逻辑,把调用结果缓存下来。

LazyObject模块
empty = object()

empty就是一个object对象,LazyObject模块会用到,表示还未实例化。

def new_method_proxy(func):
    def inner(self, *args):
        # 判断是否为empty,表示为空,未实例化
        if self._wrapped is empty:
            # 调用继承类实现的_setup,实例化类
            self._setup()
        return func(self._wrapped, *args)
    return inner

new_method_proxy是工厂函数,为其他函数增加实例化的功能。LazyObject模块内有些方法通过它实现,比如getattr

class LazyObject:
    """
    A wrapper for another class that can be used to delay instantiation of the
    wrapped class.

    By subclassing, you have the opportunity to intercept and alter the
    instantiation. If you don't need to do that, use SimpleLazyObject.
    """

    # Avoid infinite recursion when tracing __init__ (#19456).
    _wrapped = None

    def __init__(self):
        # Note: if a subclass overrides __init__(), it will likely need to
        # override __copy__() and __deepcopy__() as well.
        # 初使化的时候,把_wrapped设置为empty,表示为空,未实例化
        self._wrapped = empty

    # 通过工厂函数生成__getattr__方法,为其判断是否实例化,如果未实例化,则调用_setup实例化类
    __getattr__ = new_method_proxy(getattr)

    def __setattr__(self, name, value):
        if name == "_wrapped":
            # Assign to __dict__ to avoid infinite __setattr__ loops.
            self.__dict__["_wrapped"] = value
        else:
            # 判断是否实例化,如果未实例化,则调用_setup实例化类
            if self._wrapped is empty:
                self._setup()
            setattr(self._wrapped, name, value)

    def __delattr__(self, name):
        if name == "_wrapped":
            raise TypeError("can't delete _wrapped.")
        # 判断是否实例化,如果未实例化,则调用_setup实例化类
        if self._wrapped is empty:
            self._setup()
        delattr(self._wrapped, name)

    # 需要子类实现自己的_setup
    def _setup(self):
        """
        Must be implemented by subclasses to initialize the wrapped object.
        """
        raise NotImplementedError('subclasses of LazyObject must provide a _setup() method')
    ...

    __bytes__ = new_method_proxy(bytes)
    __str__ = new_method_proxy(str)
    __bool__ = new_method_proxy(bool)

    # Introspection support
    __dir__ = new_method_proxy(dir)

    # Need to pretend to be the wrapped class, for the sake of objects that
    # care about this (especially in equality tests)
    __class__ = property(new_method_proxy(operator.attrgetter("__class__")))
    __eq__ = new_method_proxy(operator.eq)
    __ne__ = new_method_proxy(operator.ne)
    __hash__ = new_method_proxy(hash)

    # List/Tuple/Dictionary methods support
    __getitem__ = new_method_proxy(operator.getitem)
    __setitem__ = new_method_proxy(operator.setitem)
    __delitem__ = new_method_proxy(operator.delitem)
    __iter__ = new_method_proxy(iter)
    __len__ = new_method_proxy(len)
    __contains__ = new_method_proxy(operator.contains)

上面这段代码是 LazyObject的核心逻辑,初始化的时候把_wrapped设置为empty,未对实际的类进行实例化操作,达到延迟类实例化的效果。只有对结果需要进行某些操作的时候,比如获取属性,才会去调用_setup进行实例化。而且_setup必须由子类实现。

所以如果想要让某个类有延迟实例化的功能,必须做两件事情,1)继承LazyObject;2)实现_setup方法。下面是个简单的例子

# 有延迟实例化需求的类DemoObject
class DemoObject(object):  
    def __init__(self):  
        self.title = 'just a demo'  

# 因为继承了LazyObject,所以DemoLazyObject有延迟实例化的功能
class DemoLazyObject(LazyObject):
    # 子类需要重新实现_setup方法,该方法实例化类DemoObject,并把生成的对象赋给_wrapped。
    def _setup(self):
        self._wrapped = DemoObject()  

# 实例化类DemoLazyObject,生成对象a_demo,此时并没有实际实例化类DemoObject。
a_demo = DemoLazyObject()
# 调用a_demo.title的时候,会触发类DemoObject的实例化
print(a_demo.title)

这个例子里的DemoLazyObject只有继承了LazyObject,同时重新实现_setup后,才能有延迟实例化的功能。

当实例化类DemoLazyObject时,并没有真正去实例化类 DemoObject,它的实例化在_setup里面。只有执行了后面那行print(a_demo.title),才会真正触发类 DemoObject的实例化。

第一次的调用流程是print(a_demo.title)->LazyObject.__getattr__->new_method_proxy(getattr)->DemoLazyObject._setup(self)->DemoObject.__init__(self)->LazyObject.getattr(self._wrapped, *args)

以后再调用的时候就不会进入DemoLazyObject._setup(self)DemoObject.__init__(self),而是直接返回LazyObject.getattr(self._wrapped, *args)

总结

lazy和LazyObject虽然是针对两种不同场景的惰性加载,但是它们延迟计算的实现基本差不多。都是对实际结果的属性进行重载来实现延迟计算的效果,比如这三种和属性相关的方法,__getatrr__, __setatrr__, __delatrr__。当我们使用结果的时候,一般会去获取其属性或者修改属性,所以这是一个适合触发实际计算的地方。也就是只有需要用的时候,才实例化它或者调用相关的函数获得计算结果。

推荐阅读更多精彩内容