Python导入搜索路径:发生了什么?首先会发生什么?

3
Python文档中有两个部分涉及到导入,措辞似乎有些模糊不清。
来自"The Module Search Path"
当导入名为spam的模块时,解析器首先搜索具有该名称的内置模块。如果未找到,则在变量sys.path给出的目录列表中搜索名为spam.py的文件。
来自"The Module Cache"
在导入搜索期间检查的第一个位置是sys.modules。此映射用作已导入所有模块(包括中间路径)的缓存。
这两个部分哪一个更准确地表示Python的导入系统在内部发生了什么?根据下面的逻辑,它们无法共存,因为sys.modules很可能包含不是内置的模块,并且可能排除某些模块。
这是我的困惑所在: sys.modules 用于缓存已导入的模块,而不是专门用于存储内置模块的全面列表。(我认为最接近的是 sys.built_in_modules,但它也不包括具有 .__file__ 属性的东西,如 math。)
如果我启动一个新的解释器会话,sys.modules 包含大多数内置模块,但不包括一些来自 sys.builtin_module_names 的内容:即 gctime 等。此外,您可以导入第三方包,这些包将被放置到 sys.modules 中,此时 sys.modules 明显不再是只包含内置模块的字典。因此,所有这些都似乎表明 "sys.modules != 内置模块"。

1
试图理解。sys.modules != built in modules 是令人困惑的地方。但是缓存和搜索路径是导入系统的两个不同组件。它们都有各自的作用,那么为什么一个比另一个更准确呢?这些组件有不同的角色。 - pyeR_biz
@Poppinyoshi 是的,我知道 sys.modules != 内置模块。这一点是显而易见的。我的困惑在于这两个摘录都明确表示,“搜索的第一个位置是…”或“搜索的第一件事是…”。这两个陈述如何同时正确,因为它们似乎相互冲突? - Brad Solomon
3个回答

1

您正在查看两个完全不同的信息来源,教程和语言参考。


教程部分模块搜索路径(除了描述默认行为之外)还仅描述了当模块实际导入时发生的情况。

如果模块已经在缓存中,此过程不会发生。这里没有解释,因为它在前一节更多关于模块中已经涵盖:

模块可以包含可执行语句和函数定义。这些语句旨在初始化模块。它们只在第一次遇到模块名称时执行导入语句。

...

注意:出于效率考虑,每个模块在解释器会话中仅导入一次。

它没有解释这是如何发生的机制,因为这只是一个教程。


同时,在导入系统的参考文档中,模块缓存部分解释了import语句执行的第一件事情。
请注意,并不完全正确的是Python会避免执行已经被导入的模块语句,或者仅为提高效率而导入一次。这是因为默认加载器将模块放入sys.modules缓存中的结果。如果您替换了加载器,或在事后对缓存进行修改,则一个模块实际上会被导入和执行多次。
随后的各个部分——从下一部分查找器和加载器开始——以更严格和详细的方式描述了如何找到模块,比教程中的“模块搜索路径”部分更加详细:

Python包括许多默认的查找器和导入器。第一个知道如何定位内置模块,第二个知道如何定位冻结模块。第三个默认查找器在导入路径中搜索模块。

所以,再次强调,解释器不是首先搜索内置模块。相反,解释器只是按顺序搜索其查找器,默认情况下,第一个查找器是内置模块查找器。但如果您更改查找器列表,则Python将不会首先搜索内置模块。


事实上,如果您在默认安装的CPython 3.7上打印sys.meta_path,您将看到:
<class '_frozen_importlib.BuiltinImporter'>
<class '_frozen_importlib.FrozenImporter'>
<class '_frozen_importlib_external.PathFinder'>

(在IPython下,或者如果您导入了像six这样帮助重命名模块的东西,或者如果您导入了像requests这样嵌入版本化模块的内容,您将拥有一些额外的查找器。)
那个BuiltinImporterimportlib库文档中有记录。(如果您想知道为什么它不叫做BuiltinFinder,一个既是其自己的查找器又是其自己的加载器的查找器被称为导入器。) 它实际上所做的是查看sys.builtin_module_names并调用一个特定于实现的函数来处理其中找到的任何内容。

在CPython 3.6(抱歉来回跳转3.6和3.7,但这里不应该有影响...)中,它调用的是特定于实现的函数{{link2:_imp.create_builtin}},您可以从那里追踪事物。

但需要注意的关键是,并非builtin_module_names中的所有内容都是预先导入的意义上的“内置”。例如,通过正常安装,您可能会看到_ast,但没有sys.modules['_ast']

因此,create_builtin函数(或对于不同的实现,用于实现BuiltinImporter的任何内容)必须能够导入随Python预安装的so/dll/pyd/dylib模块。


@BradSolomon,“搜索内置模块”的含义(在3.4+中)有点复杂;我会编辑答案来涵盖这一点。 - abarnert

1
当你使用import导入一个模块时,解释器首先搜索内置模块,然后搜索sys.path。但这仅适用于真正导入模块的情况。在导入模块之前,有一个缓存需要搜索。如果模块已经在缓存中,则不会再次导入。

0

你需要区分sys.pathsys.modules

sys.modules 这是一个将模块名称映射到已加载的模块的字典。这可以被操作以强制重新加载模块和其他技巧。请注意,从此字典中删除模块与在相应的模块对象上调用reload()不同。

当我在jupyter笔记本中加载sys.path时,会显示一个将加载的模块名称映射到文件位置的字典 -

{'IPython': <module 'IPython' from 'C:\\Users\\User\\Anaconda3\\lib\\site-packages\\IPython\\__init__.py'>,
 'IPython.core': <module 'IPython.core' from 'C:\\Users\\User\\Anaconda3\\lib\\site-packages\\IPython\\core\\__init__.py'>,.....}

这是我的模块缓存,但是当我尝试时

sys.modules['numpy']

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-6-44b02d746fe5> in <module>()
----> 1 sys.modules['numpy']

KeyError: 'numpy'

由于numpy不在我的模块缓存中,我将请求Python查找它是否在一组固定目录中,这些目录在sys.path中定义。这是一个字符串列表,我可以根据需要添加或删除路径。

sys.path是一个字符串列表,指定模块的搜索路径。从环境变量PYTHONPATH初始化,加上安装相关的默认值。

如果Python在我的sys.path集合中找到库,则会在我的sys.modules中为其创建映射,以便在活动环境中快速访问。

import numpy
sys.modules['numpy']
#<module 'numpy' from 'C:\\Users\\User\\Anaconda3\\lib\\site-packages\\numpy\\__init__.py'>

1
这一切都是真的,但它并不特别相关于楼主的问题。他似乎完全没有对sys.modulessys.path的作用感到困惑。(他可能会对sys.pathsys.meta_path之间的区别感到困惑,但您的回答根本没有涉及到这一点。) - abarnert
@abarnert 感谢您的反馈和回答。我想知道问题是否可以直接从“这就是我的困惑所在”的地方开始。因为它涉及到Python搜索模块的顺序,而不是缓存与路径哪个“更准确地表示了Python导入系统内部发生的情况?”无论如何,感谢您,不想在这上面浪费时间。 - pyeR_biz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接