开源项目源码解读--lazyload, 惰性加载模块

1. lazyload

lazyload模块,可以实现模块的惰性加载。所谓惰性加载,是指在使用import引入模块时,并不真正的引入,真正的引入发生获取模块的某个属性时。

项目地址: https://github.com/thomasballinger/lazyload, 下面是作者提供的测试代码

import lazyload
lazyload.make_lazy('requests')
import requests # nearly instant
requests.get    # takes 1.2 seconds

make_lazy方法将requests模块设置为惰性加载模块,因此下一行代码import requests 并不会真正的引入该模块,最后一行代码尝试方法这个模块的get函数,此时,才会真正的引入该模块。

让模块具备惰性加载的特性,可以避免程序在启动时,因大量的import操作导致启动时间过长,模块导入的时间并没有减少,只是被分散到了获取模块某个具体属性的操作上。在生产环境中是否有实践意义,我暂时难以评估。

2. 实现原理

该模块目前只支持python 2.7, 3.4, 3.5 这3个版本,这是其实现原理决定的。虽然不支持高版本,但阅读其源码,仍然可以帮助你理解python在模块引入上的一些细节。

2.1 make_lazy

make_lazy函数是模块的核心,结合测试代码,make_lazy('requests')将requests模块具备惰性加载的特性,在import requests 语句执行时,真正引入的,并不是requests,而是make_lazy内部定义的LazyModule类的实例。

以下两行代码是实现这一效果的关键

sys_modules = sys.modules   # 29行

sys_modules[module_path] = LazyModule()     # 61行

一个模块被import语句引入时,首先会检查是否已经存在于sys.modules中,如果存在,则直接从sys.modules获取,不存在,则引入模块并存储在sys.modules中。

sys.modules是一个字典结构,key是模块的名称,value是模块。

make_lazy('requests') 的执行先于 import requests,如此一来,sys_modules中便存在了一个key为requests,value为LazyModule实例的key-value对。那么当import requests 被执行时,实际引入的是LazyModule实例,而非真正的requests模块。

2.2 LazyModule

make_lazy函数创建一个LazyModule实例对象,并通过sys.modules将这个实例伪装成用户想要导入的模块。
当requests.get被执行时,LazyModule实例的__getattribute__方法将被执行,这并不难理解,这种object.attribute的写法,实际调用的正是这种object的__getattribute__方法。

让我们来看一下__getattribute__ 方法

    def __getattribute__(self, attr):
        if module.value is None:
            del sys_modules[module_path]
            module.value = __import__(module_path)

            sys_modules[module_path] = __import__(module_path)

        return getattr(module.value, attr)

module对象是在make_lazy 函数的32行被创建的: module = NonLocal(None), 当requests.get被执行时,module.value 一定为None, 在if语句块里,做了3个操作

  1. del sys_modules[module_path], 删除第61行创建的key-value对,value是LazyModule实例,并不是用户想要的requests模块
  2. module.value = import(module_path), 导入真正的requests, 并赋值给module.value
  3. sys_modules[module_path] = import(module_path), 在sys.modules存放真正的requests模块

最后一步,使用getattr函数返回get函数: return getattr(module.value, attr)

2.3 __mro__ 方法

我并没有真正理解__mro__ 方法在LazyModule类里的作用,不过没关系,我们阅读源码的目的,不是完全彻底的搞懂源码里的每一行代码,而是要通过阅读优秀的源码,来丰富自己的知识,提高自己的水平。虽然不理解在源码里的作用,我们却可以借此机会学习它的用法。

__mro__ 方法的作用,是决定多继承情形下方法解析的顺序。

class A():
    def foo(self):
        print('a')


class B():
    def foo(self):
        print('b')


class C(A, B):
    pass


c = C()
c.foo()

C继承了A和B,A和B都实现了foo方法,那么c.foo()在执行时,究竟是调用A的foo方法还是B的foo方法呢? 大概率你会去猜调用A的foo,虽然结论是对的,但猜总归是猜,我们还是要从根本上解释它。

多继承情况下,不论继承关系有多复杂,类的__mro__属性,都可以明确的指出方法解析的顺序,mro 是 Method Resolution Order的缩写

print(C.__mro__)    # (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)

你可以可以用C.mro() 获得解析顺序,它返回的是一个列表。在这个顺序里,A更靠近C, 因此c.foo, 调用的是A类的foo方法。

在lazyload源码里,作者在注释里解释说重写了__mro__方法,可在3.6中,__mro__是属性,而非方法,或许在3.4, 3.5版本里,它这样做是ok的。

3. 收获总结

  1. 了解了sys.modules的功能和作用,以及引入模块时的基本过程
  2. 接触到了__getattribute__ 方法和getattr函数
  3. 学习到了mro,这对于理解多继承情况下,子类调用父类方法的顺序十分有帮助

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案