• Python3.7中一种懒加载的方式
  • 发布于 2个月前
  • 154 热度
    0 评论
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 importlib
def lazy_import(importer_name, to_import):
    """Return the importing module and a callable for lazy importing.
    The module named by importer_name represents the module performing the
    import to help facilitate resolving relative imports.
    to_import is an iterable of the modules to be potentially imported (absolute
    or relative). The `as` form of importing is also supported,
    e.g. `pkg.mod as spam`.
    This function returns a tuple of two items. The first is the importer
    module for easy reference within itself. The second item is a callable to be
    set to `__getattr__`.
    """
    module = importlib.import_module(importer_name)
    import_mapping = {}
    for name in to_import:
        importing, _, binding = name.partition(' as ')
        if not binding:
            _, _, binding = importing.rpartition('.')
        import_mapping[binding] = importing

    def __getattr__(name):
        if name not in import_mapping:
            message = f'module {importer_name!r} has no attribute {name!r}'
            raise AttributeError(message)
        importing = import_mapping[name]
        # imortlib.import_module() implicitly sets submodules on this module as
        # appropriate for direct imports.
        imported = importlib.import_module(importing,
                                           module.__spec__.parent)
        setattr(module, name, imported)
        return imported
    return module, __getattr__

你可以这样使用上面的代码
# In pkg/__init__.py with a pkg/sub.py.
mod, __getattr__ = lazy_import(__name__, {'sys', '.sub as thingy'})
def test1():
    return mod.sys
def test2():
    return mod.thingy.answer
设计这个函数时,最棘手的部分就是模拟import ... as ... 语法来避免命名冲突,我最终选择使用一个类似原有as语法的字符串。我也可以把as语法字符串再拆分为第三个参数,这个参数也是一个字典对象,但是我想没必要这样做,能与原有语法有更多的相同点,当然是最好的。

无论如何,这个思考的过程都让我很享受。我喜欢这种用20行Python代码就完成一个不错的功能的感觉!

用户评论