Python模块导入机制与惰性导入实现详解
本文深入解析Python模块导入机制,涵盖模块与包的区别、常规包与命名空间包的特性,以及__import__和importlib模块的高级用法。重点探讨如何通过sys.modules缓存机制实现模块的惰性导入,以优化内存占用和启动速度,并提供完整的LazyModuleType实现代码。
本文不涉及Python导入机制的底层实现细节,仅聚焦于模块与包的概念,以及导入语句相关的知识。通常,导入模块的标准写法如下:
import ...
import ... as ...
from ... import ...
from ... import ... as ...
在大多数常规场景下,使用上述标准导入语句已足够满足需求。然而,在特定复杂场景中,可能需要采用其他导入方式。例如,Python提供了__import__内建函数和importlib模块来支持动态导入。动态导入的核心优势在于能够实现模块加载的延迟,即仅在模块实际被使用时才执行导入操作。
虽然利用__import__函数和importlib模块可以实现模块的延迟加载,但其缺点在于需要在每个需要使用指定模块的地方重复编写相同的导入逻辑,这不仅增加了维护成本,也显得繁琐。若能在顶层实现惰性导入,将是一个更优的选择,这也是本文最终要探讨的核心内容。
在深入探讨高级用法之前,首先需要明确模块与包的基本概念。
模块与包
模块可以理解为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。
命名空间包的__path__属性与常规包不同,它使用自定义的迭代器类型来遍历所有包含该命名空间包的路径。如果其父包的路径(或高阶包的sys.path)发生改变,它将在下次尝试导入时自动在该包中重新搜索包部分。
假设存在如下目录结构:
. ├── 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'])
命名空间包具有以下特性:
1、优先级最低,在所有其他import规则之后生效
2、包中不必再包含__init__.py文件
3、可以导入并组织分散在目录中的代码
4、依赖于sys.path中从左到右的搜索顺序
__import__
__import__函数可用于导入模块,import语句也会调用该函数。其定义如下:
__import__(name[, globals[, locals[, fromlist[, level]]]])
参数介绍:
- name (required): 被加载module的名称
- globals (optional): 包含全局变量的字典,该选项很少使用,采用默认值 global()
- Locals (optional): 包含局部变量的字典,内部标准实现未用到该变量,采用默认值 - local()
- fromlist (Optional): 被导入的submodule名称
- level (Optional): 导入路径选项,Python 2中默认为-1,表示同时支持absolute import和relative import。Python 3中默认为0,表示仅支持absolute import。如果大于0,则表示相对导入的父目录的级数,即1类似于'.',2类似于'..'。
使用示例:
# 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与importlib模块
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)
总结
到此这篇关于Python中模块导入的文章就介绍到这了,更多相关Python模块导入内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
参考资料
- https://docs.python.org/3/reference/import.html
- https://github.com/nipy/nitime
- https://github.com/mnmelo/lazy_import
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









