
Python import系统与模块加载的细节import语句的执行过程远比表面上复杂。Python解释器执行import foo时经历了一系列步骤查找、加载、初始化、缓存。完整的import流程由importlib模块实现。Python 3.3之后整个import系统用纯Python重写移除了C层面的__import__函数的大部分逻辑。第一步查找模块。解释器检查sys.modules缓存。如果模块在缓存中直接返回。否则进入查找阶段import sysprint(sys.modules.keys()) # 所有已加载模块print(math in sys.modules) # False还没导入import mathprint(math in sys.modules) # Truesys.modules是一个普通的dict。可以手动操作它来干预导入过程# 模拟模块缓存class MockModule:def __init__(self):self.name mock_versionsys.modules[real_module] MockModule()import real_moduleprint(real_module.name) # mock_version预先在sys.modules中插入对象后import real_module不再执行真正的模块加载直接返回插入的对象。这是猴子补丁monkey patching模块的常用手段。查找阶段由sys.meta_path中的finder来执行。默认情况下包含三个finderimport sysfor finder in sys.meta_path:print(finder)第一个是importlib.machinery.BuiltinImporter处理内建模块如sys、math。第二个是importlib.machinery.FrozenImporter处理冻结模块用_FROZEN编译的Python模块。第三个是importlib.machinery.PathFinder基于sys.path查找文件系统中的模块。sys.path的构成import sysprint(sys.path)顺序是当前脚本所在目录、PYTHONPATH环境变量、site-packages目录、标准库路径。PathFinder依次搜索sys.path中的每个目录。PathFinder在每个目录中查找与模块名匹配的.py、.pyc、.pyd、.so文件。查找顺序由sys.path_hooks和sys.path_importer_cache控制。Finder找到模块后返回一个ModuleSpec对象描述如何加载模块import importlib.utilspec importlib.util.find_spec(json)print(spec.name) # jsonprint(spec.origin) # /usr/lib/python3.11/json/__init__.pyprint(spec.loader) #print(spec.submodule_search_locations) # 包子模块的位置ModuleSpec包含loader加载器、origin来源路径、submodule_search_locations包的搜索路径等字段。加载器执行实际的模块代码执行和对象创建工作。包和普通模块的区别在于子模块搜索路径。包有一个特殊的__path__属性import jsonprint(json.__path__) # [/usr/lib/python3.11/json]__path__的存在告诉import系统这个包包含子模块。__path__可以动态修改实现命名空间包# 不同目录可以在同一个包名下合并# 目录A/pkg/module_a.py# 目录B/pkg/module_b.py# 设置PYTHONPATH为A:B# import pkg.module_a 和 import pkg.module_b 都能成功命名空间包是Python 3.3之后PEP 420的特性。多个目录可以贡献到同一个包名下不需要__init__.py文件。模块加载阶段Loader执行模块的代码。SourceFileLoader读取.py文件编译成字节码在模块的命名空间中执行。执行完成后模块对象被插入sys.modules。缓存阶段加载完成的模块被缓存到sys.modules。后续导入直接从缓存获取不再重复加载。缓存的一个副作用如果动态删除了sys.modules中的条目模块对象仍然存活如果有其他引用但再次导入时会重新执行模块代码。模块重载使用importlib.reloadimport importlibimport mymodule# 修改mymodule.py后importlib.reload(mymodule)reload重新执行模块代码但不会重新初始化模块对象本身。已有的类实例、全局变量等不会被重置除非模块代码显式处理。importlib.reload的局限性它不会更新from导入的引用from mymodule import some_functionimportlib.reload(mymodule)# some_function还是指向旧函数原因是from导入将对象引用直接绑定到当前命名空间中reload创建的新对象不会影响已存在的绑定。__init__.py的导入控制# package/__init__.pyfrom .submodule import useful_function__all__ [useful_function, important_class]# 用户使用import packagepackage.useful_function() # OKpackage.some_other_function() # AttributeError__all__控制from package import *的行为而__init__.py中显式导入的名字成为包的命名空间的一部分。相对导入的限制# package/sub/module.pyfrom .. import parent_module # OKfrom ...other_package import something # 不能超过包根目录相对导入只能在包内部使用并且不能超出包的顶级目录。__name__变量的值决定了包的层级结构# package/sub/module.pyprint(__name__) # package.sub.moduleprint(__package__) # package.sub如果直接运行python package/sub/module.py__name__是__main____package__是None相对导入会失败。这就是为什么包内的模块不能用脚本方式运行。Python 3.11对import做了性能优化冻结模块缓存。标准库的模块被预编译为C代码嵌入解释器中省去了文件系统和编译的开销import _impprint(_imp.is_frozen(importlib)) # TruePython启动时importlib和它的依赖模块从冻结的字节码加载不经过文件读取和编译。这让Python命令的启动时间减少了约30%。sys.path的动态操作会在运行时生效# 添加搜索路径sys.path.insert(0, /path/to/custom/modules)import my_custom_module # 优先从/path/to/custom/modules查找# 删除搜索路径sys.path.remove(/path/to/problematic/modules)但动态修改sys.path在多线程环境下不安全因为多个线程可能同时遍历sys.path。用importlib重新加载系统路径import importlib.machineryimportlib.machinery.PathFinder.invalidate_caches()invalidate_caches清理文件系统缓存让PathFinder重新扫描目录。这在新安装包后特别有用。自定义Finder用于实现远程导入import importlib.abcimport importlib.utilimport urllib.requestclass RemoteFinder(importlib.abc.MetaPathFinder):def __init__(self, base_url):self.base_url base_urldef find_spec(self, fullname, path, targetNone):url f{self.base_url}/{fullname.replace(., /)}.pytry:response urllib.request.urlopen(url)if response.status 200:source response.read().decode(utf-8)loader RemoteLoader(source)return importlib.util.spec_from_loader(fullname, loader,originurl)except urllib.error.HTTPError:return Noneclass RemoteLoader(importlib.abc.Loader):def __init__(self, source):self.source sourcedef create_module(self, spec):return None # 使用默认的模块创建方式def exec_module(self, module):code compile(self.source, module.__spec__.origin, exec)exec(code, module.__dict__)sys.meta_path.insert(0, RemoteFinder(https://example.com/pymodules))import remote_module # 从远程服务器加载MetapathFinder在sys.meta_path中的顺序决定优先级。插入到列表前面优先于内建模块查找器。插入到后面只在本地找不到时才尝试远程加载。模块的__getattr__函数允许在模块上动态定义属性。Python 3.7引入# module_with_fallback.pydef __getattr__(name):if name deprecated_function:import warningswarnings.warn(deprecated_function is deprecated, DeprecationWarning)from .new_module import new_functionreturn new_functionraise AttributeError(fmodule {__name__!r} has no attribute {name!r})在模块中定义顶层__getattr__函数当访问模块中不存在的属性时触发。用于实现废弃函数的向前兼容、延迟加载大型模块等功能# lazy_module.pyimport importlib_LAZY {}def __getattr__(name):if name in _LAZY:module_name _LAZY[name]module importlib.import_module(module_name)return getattr(module, name)raise AttributeError(fmodule {__name__!r} has no attribute {name!r})# 注册lazy加载_LAZY[Image] PIL.Image_LAZY[DataFrame] pandas.DataFrame_LAZY字典中注册了延迟加载的映射关系。首次访问Image或DataFrame时触发__getattr__真正导入对应的模块并返回需要的对象。Python 3.10对import的错误信息做了大幅改进。导入失败的提示更加具体try:import nonexistent_moduleexcept ImportError as e:print(e.name) # nonexistent_moduleprint(e.path) # 出错时的搜索路径ImportError的name和path属性帮助定位问题。Python 3.10还增加了did you mean提示当导入名接近某个标准库时给出建议。import foo # ImportError: No module named foo# 3.10会提示: Did you mean: functools, fileinput, fonttools?zipimporter可以从zip文件中导入模块import syssys.path.insert(0, /path/to/modules.zip)import some_module_from_zipzip文件内的模块不需要解压。PathFinder的PathEntryFinder分支会根据sys.path中的zip文件创建ZipImporter对象。Python 3.12引入了multi-phase extension module init。C扩展模块可以分两步初始化先创建模块对象再填充成员。这改进了导入性能并减少了初始化时的内存峰值。importlib.metadata提供了解析包元数据的能力from importlib.metadata import version, metadata, requiresprint(version(requests)) # 2.28.1print(metadata(requests)[Author]) # Kenneth Reitzprint(requires(requests)) # 依赖列表importlib.metadata读取已安装包的METADATA文件不需要import包本身。这个模块替代了pkg_resources库。