注意:这篇博文中的代码,现在已经是PyPI上modutil模块的一部分了。
Python3.7在模块上也添加了__getattr__()和__dir__()两个方法。这个新特性让我们能够实现一些有趣的事情。例如,通过定义__dir__方法,你可以要求dir(模块)只显示__all__中定义的内容了。
但对于我来说,我更关心__getattr__方法,以及我如何能利用这个特性实现懒加载。在本文的开始,我希望先告诉大家,大多数人的代码是不需要懒加载的。只有当懒加载带来的好处很有用时你才应该使用它,比如,一个运行时间很短的终端程序。对于大多数人,懒加载都是没有必要的,甚至是有害的,它会让你延后得知导入失败,而不是项目一启动时就能够知道。
旧方法
之前我们有两种方式做懒加载。最古老的方式是在局部加载,而不是全局加载(例如在你定义的函数内进行导入,而不是在模块顶部进行导入)。这种方式确实推迟了加载的时间点,在你的函数被运行时,函数里的import语句才会进行真正的加载。但它有一个缺点就是,这个import语句需要在不同的函数中写很多次。而且由于你只是在一些函数中写了import语句,你很可能写着写着就忘记了想要规避哪个模块的全局引用,然后后面又不小心全局引用了同样的模块。这个做法确实能实现懒加载,就是写法不够好。
另一种方法是使用importlib中提供的延迟加载器。Google、Facebook和Mercurial的很多开发者在使用这个延迟加载器。Google和Facebook主要是看中这个方法性能不错,Mercurial主要是看中这个方法比较简单、开发迅速。使用这个延迟加载器有一个很好的效果,它会提前检查要导入的模块是否能找到,如果找不到会抛出一个ModuleNotFoundError错误,而真正被延迟的只是模块加载的过程。
很多人很喜欢这个延迟加载器,以至于他们让所有的东西都延迟加载了。这样做有优点也有缺点。优点是你没有额外付出什么努力,就让所有的模块都延迟加载了。缺点是因为你让模块默认延迟加载了,会导致一些需要即时加载的模块的逻辑发生错误(这也就是Python箴言中为什么说明确优于隐晦)。事实上,Mercurial为了避免这个问题,专门维护了一个模块黑名单,黑名单上的模块不进行延迟加载。但为此,他们不得不一直维护这个名单,所以这样做也不是一个很好的办法。
新方法
在Python3.7中,模块上可以定义一个__getattr__方法,这让开发者可以定义一个函数,使得访问的模块属性不存在时,导入一个模块作为当前模块的属性。这样做确实也有“发现导入错误被推迟”这个弊病,但是由于你的导入还是全局的,所以代码更容易控制。
这个代码本身并不复杂。
你可以这样使用上面的代码
设计这个函数时,最棘手的部分就是模拟import ... as ... 语法来避免命名冲突,我最终选择使用一个类似原有as语法的字符串。我也可以把as语法字符串再拆分为第三个参数,这个参数也是一个字典对象,但是我想没必要这样做,能与原有语法有更多的相同点,当然是最好的。
无论如何,这个思考的过程都让我很享受。我喜欢这种用20行Python代码就完成一个不错的功能的感觉!