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__。当我们使用结果的时候,一般会去获取其属性或者修改属性,所以这是一个适合触发实际计算的地方。也就是只有需要用的时候,才实例化它或者调用相关的函数获得计算结果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容