本文不讨论 Python 的导入机制(底层实现细节),仅讨论模块与包,以及导入语句相关的概念。一般,导入模块都是使用以下语句:html
import ... import ... as ... from ... import ... from ... import ... as ...
通常状况下,使用以上语句导入模块已经够用的。可是在一些特殊场景中,可能还须要其余的导入方式。例如 Python 还提供了 __import__
内建函数和 importlib 模块来实现动态导入。动态导入的好处是能够延迟模块的加载,仅在用到模块时才支持导入动做。python
运用 __import__
函数和 importlib 模块当然可以实现模块的延迟加载,但其不足之处是,在任何须要用到指定模块的地方都要实现一遍一样的导入语句,这样是不便于维护且很是麻烦的。若是可以在顶层实现惰性导入,则是一个更好的选择,这也是本文最终要讨论的点。git
在讨论一些高级用法以前,首先须要了解一下模块与包的概念。github
模块 能够理解为是 Python 能够加载并执行的代码文件,代码文件不只能够是 .py
文件,还能够是 .so
等其余类型的文件。Python 只有一个 模块 对象型态,并且全部模块都是这个型态。为了便于组织多个模块并提供一个模块层次结构的命名,Python 提供了 包 的概念。缓存
能够简单的将包看做是一个文件系统的目录,将模块看做是目录中的代码文件(注意,不能彻底地这样认为,由于包和模块并不是仅来自文件系统,还能够来自压缩文件、网络等)。相似于文件系统的目录结构,包被分级组织起来,并且包自己也能够包含子包和常规模块。网络
包其实能够看做是一种特殊的模块。例如常规包(下面会介绍常规包的概念)的目录中须要包含 __init__.py
文件,当包被导入时,该文件的顶层代码被隐式执行,就如同模块导入时顶层代码被执行,该文件就像是包的代码同样。因此 包是一种特殊的模块。须要记住的是,全部的包都是模块,但不是全部的模块都是包。包中子包和模块都有 __path__
属性,具体地说,任何包含 __path__
属性的模块被认为是包。全部的模块都有一个名称,相似于标准属性访问语法,子包与他们父包的名字之间用点隔开。函数
Python 定义了两种类型的包,即 常规包 和 命名空间包。常规包是存在于 Python 3.2 及更早版本中的传统包。常规包即包含 __init__.py
文件的目录。当导入一个常规包时,__init__.py
文件被隐式执行,并且它定义的对象被绑定到包命名空间中的名称。 __init__.py
文件能包含其余任何模块可以包含的相同的 Python 代码,并且在导入时,Python 将给模块增长一些额外的属性。测试
从 Python 3.3 开始,Python 引入了 命名空间包 的概念。命名空间包是不一样文件集的复合,每一个文件集给父包贡献一个子包,全部的包中都不须要包含 __init__.py
文件。文件集能够存于文件系统的不一样位置。文件集的查找包含导入过程当中 Python 搜索的压缩文件,网络或者其余地方。命名空间包能够但也能够不与文件系统的对象直接对应,它们能够是真实的模块但没有具体的表述。更新关于命名空间包的说明能够参考 PEP 420。ui
命名空间包的 __path__
属性与常规包不一样,其使用自定义的迭代器类型,遍历全部包含该命令空间包的路径。若是他们父包的路径(或者高阶包的 sys.path )改变,它将在下次试图导入时在该包中自动从新搜索包部分。spa
若有以下目录结构:
. ├── bar-package │ └── nsp │ └── bar.py └── foo-package └── nsp └── foo.py
则 nsp 便可以是一个命名空间包,如下是测试代码(记得用 Python 3.3 及更高版本运行测试):
import sys sys.path.extend(['foo-package', 'bar-package']) import nsp import nsp.bar import nsp.foo print(nsp.__path__) # 输出: # _NamespacePath(['foo-package/nsp', 'bar-package/nsp'])
命名空间包具备以下特性:
__init__.py
文件__import__
__import__
函数可用于导入模块,import 语句也会调用函数。其定义为:
__import__(name[, globals[, locals[, fromlist[, level]]]])
参数介绍:
使用示例:
# import spam spam = __import__('spam') # import spam.ham spam = __import__('spam.ham') # from spam.ham import eggs, sausage as saus _temp = __import__('spam.ham', fromlist=['eggs', 'sausage']) eggs = _temp.eggs saus = _temp.sausage
在执行模块导入时,Python 的导入系统会首先尝试从 sys.modules 查找。sys.modules
中是全部已导入模块的一个缓存,包括中间路径。即,假如 foo.bar.baz 被导入,那么,sys.modules 将包含进入 foo,foo.bar 和 foo.bar.baz 模块的缓存。其实一个 dict 类型,每一个键都有本身的值,对应相应的模块对象。
导入过程当中,首先在 sys.modules 中查找模块名称,若是存在,则返回该模块并结束导入过程。若是未找到模块名称,Python 将继续搜索模块(从 sys.path 中查找并加载)。sys.modules 是可写的,删除一个键会使指定模块的缓存实现,下次导入时又将从新搜索指定的模块,这相似于模块的 reload。
须要注意的是,若是保持模块对象引用,并使 sys.modules 中缓存失效,而后再从新导入指定的模块,则这两个模块对象是不相同的。而相比之下,importlib.reload()
从新加载模块时,会使用相同的模块对象,并经过从新运行模块代码简单地从新初始化模块内容。
imp
模块提供了一些 import 语句内部实现的接口。例如模块查找(find_module)、模块加载(load_module)等等(模块的导入过程会包含模块查找、加载、缓存等步骤)。能够用该模块来简单实现内建的 __import__
函数功能:
import imp import sys def __import__(name, globals=None, locals=None, fromlist=None): # 首先从缓存中查找 try: return sys.modules[name] except KeyError: pass # 若是模块缓存中没有,则开始从 sys.path 中查找模块 fp, pathname, description = imp.find_module(name) # 如何找到模块则将其载入 try: return imp.load_module(name, fp, pathname, description) finally: if fp: fp.close()
importlib
模块在 python 2.7 被建立,而且仅包含一个函数:
importlib.import_module(name, package=None)
这个函数是对 __import__
的封装,以用于更加便捷的动态导入模块。例如用其实现相对导入:
import importlib # 相似于 'from . import b' b = importlib.import_module('.b', __package__)
从 python 3 开始,内建的 reload 函数被移到了 imp 模块中。而从 Python 3.4 开始,imp 模块被否决,再也不建议使用,其包含的功能被移到了 importlib 模块下。即从 Python 3.4 开始,importlib 模块是以前 imp 模块和 importlib 模块的合集。
前边介绍的大部份内容都是为实现惰性导入作铺垫,其余的小部份内容仅是延伸而已(就是随便多介绍了点内容)。惰性导入即延迟模块导入,在真正用到模块时才执行模块的导入动做,若是模块不被使用则导入动做永远不会发生。
惰性导入的需求仍是很常见的。通常推荐模块仅在顶层导入,而有时候在顶层导入模块并不是最好的选择。好比,一个模块仅在一个函数或者类方法中用到时,则可以使用局部导入(在局部做用域中执行导入),使得仅在函数或方法被执行时才导入模块,这样能够避免在顶层名字空间中引入模块变量。再好比,在我工做所负责的项目中,须要用到 pandas 包,而 pandas 包导入了会占用一些内存(不是不少,但也不算少,几十兆的样子),因此当不会用到 pandas 包时,咱们但愿他不被导入。咱们本身实现的一些包在载入时会很耗时(由于要读取配置等等,在导入时就会耗时几秒到十几秒的样子),因此也极其须要惰性导入的特性。
下面是惰性导入的简单实现,可供参考:
import sys from types import ModuleType class LazyModuleType(ModuleType): @property def _mod(self): name = super(LazyModuleType, self).__getattribute__("__name__") if name not in sys.modules: __import__(name) return sys.modules[name] def __getattribute__(self, name): if name == "_mod": return super(LazyModuleType, self).__getattribute__(name) try: return self._mod.__getattribute__(name) except AttributeError: return super(LazyModuleType, self).__getattribute__(name) def __setattr__(self, name, value): self._mod.__setattr__(name, value) def lazy_import(name, package=None): if name.startswith('.'): if not package: raise TypeError("relative imports require the 'package' argument") level = 0 for character in name: if character != '.': break level += 1 if not hasattr(package, 'rindex'): raise ValueError("'package' not set to a string") dot = len(package) for _ in range(level, 1, -1): try: dot = package.rindex('.', 0, dot) except ValueError: raise ValueError("attempted relative import beyond top-level " "package") name = "{}.{}".format(package[:dot], name[level:]) return LazyModuleType(name)