为什么 os.path 可以被直接导入

今天在微博上,@julyclyde 发现了一个看似简单但很有意思的问题,import os.path 的行为很奇怪。

Python 使用 import 语句来“导入”外部模块,而常见的情况有:

  • a 已经被导入了,a 已经被 sys.modules 记录在案,重复导入没有效果,只会返回 sys.modules 中的 现有的引用
  • a 是一个位于搜索路径中的文件,Python 导入该文件
  • a 是一个位于搜索路径中的目录,包含 __init__.py,Python 导入此 package
  • 使用 Import Hook 在 Python 的 import 语句中挂上钩子,改变 import 逻辑,实现自定义行为

在导入子模块 a.b 时,

  • a 是 package,Python 导入该 package 的子模块 b
  • a 定义了子模块 __path__ 列表,Python 根据这个搜索路径去寻找并导入 b
  • b 是 a 下的一个变量,不是子模块,因为不存在字面上名为 “a.b” 的模块,导入失败

但是 os.path 却不符合其中的任何一种条件。首先,os 是一个普通的文件, 位于 /usr/lib64/python[x].[y]/os.py,也没有 __init__.py,因此它并不是 package;通读源代码,没有发现它使用了任何 Hook,也没有定义 __path__,更不存在 os.path 这个模块,因此 import os.path 理应失败!然而,有趣的是,os.path 居然可以作为 os 的 子模块直接导入,这实在是太奇怪了。

仔细阅读 os,不难发现这段代码

if 'posix' in _names:
    ...
    import posixpath as path
    ...
elif 'nt' in _names:
    ...
    import ntpath as path
    ...
elif 'ce' in _names:
    ...
    import ntpath as path
    ...
else:
    raise ImportError('no os specific module found')

sys.modules['os.path'] = path

从中我们可以看出,os 会根据不同的系统平台导入不同的 os.path 实现模块,然后把该模块 的引用,强行插入到已导入模块 sys.modules 里。这样一样,import os.path 会优先在 sys.modules 搜索到目标,立刻返回。

问题在于,os 的代码是在何时被执行的呢?它是在 Python 启动时的 bootstrap 中执行的吗? 使用 python -v 调试,

import _frozen_importlib # frozen
import imp # builtin
import sys # builtin
# /usr/lib64/python3.4/__pycache__/os.cpython-34.pyc matches /usr/lib64/python3.4/os.py
# code object from '/usr/lib64/python3.4/__pycache__/os.cpython-34.pyc'
# /usr/lib64/python3.4/__pycache__/stat.cpython-34.pyc matches /usr/lib64/python3.4/stat.py
# code object from '/usr/lib64/python3.4/__pycache__/stat.cpython-34.pyc'
import 'stat' # <_frozen_importlib.SourceFileLoader object at 0x7f70160d1e10>
# /usr/lib64/python3.4/__pycache__/posixpath.cpython-34.pyc matches /usr/lib64/python3.4/posixpath.py
# code object from '/usr/lib64/python3.4/__pycache__/posixpath.cpython-34.pyc'
# /usr/lib64/python3.4/__pycache__/genericpath.cpython-34.pyc matches /usr/lib64/python3.4/genericpath.py
# code object from '/usr/lib64/python3.4/__pycache__/genericpath.cpython-34.pyc'
import 'genericpath' # <_frozen_importlib.SourceFileLoader object at 0x7f70160d6710>
import 'posixpath' # <_frozen_importlib.SourceFileLoader object at 0x7f70160d30b8>

>>> "os" in sys.modules
True
>>> "os.path" in sys.modules
True

可以看到,在 Python 解释器在初始化过程中,os 确实被执行了一次。而且,osos.path 存在于已导入列表中。但是,使用 -S 让 Python 不自动 import site 进行部分初始化之后,os 和 os.path 都不会自动导入了,但 import os.path 依然可以正常运作。

众所周知,在 Python 中,对象是被代码实时的创造出来,因此,import 必然要执行模块中的代码,否则变量、函数、类都不会存在,import 也就不可能导入任何东西了。但值得一提的是,在而导入子模块的时候,因此父模块的代码也会不可避免的被执行,原因之一是 Python 需要查阅 父模块的 __path__ 变量来决定子模块的位置,也就是说,考虑 hello.py

print("Hello, world!")

如果我们试图 import hello.should_not_exist

>>> import hello.should_not_exist
Hello, world  # hello.py 被执行了

# 执行完了,现在看看程序有没有定义好 `__path__`
Traceback (most recent call last):
  File "<frozen importlib._bootstrap>", line 2218, in _find_and_load_unlocked
AttributeError: 'module' object has no attribute '__path__'
# 然而 `__path__` 不存在,except AttributeError,继续尝试

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named 'hello.should_not_exist'; 'hello' is not a package
# hello 也不是一个 package,因此出错

可见,我们在 import os.path 时,首先 os 被执行,注入 os.pathsys.modules 里,然后,Python 才尝试导入 os.path,这时它 已经位于导入列表,于是得以顺利导入。因此,与其把 os.path 看作 os 的子模块,不如把它看作名为 os.path 的独立模块。根据同样的原理,我们不难在自己的项目中使用同样的技巧。

分类目录: Python

4 评论

  1. I Һave been exploring for a bit for ɑny hiցh-quality articles oor blog posts on this
    kind of ɦoᥙse . Exploring in Yɑhoo I ultimately stumƄled upon tҺis
    site. Reading thi info So i’m glaԁ tto show that I havе
    a very just right uncanny feeling Ι discovered exactly whqt I needed.
    I such a lot witһout a doubt will maқe certain to don?t fail too remember this website and give it a glance rеgularⅼy.

  2. I was ablᥱ to find good info ffom your blog articles.

  3. sh.py 更有意思,你可以 from sh import 任意你已安装的二进制程序名

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

版权所有 © 2025 比尔盖子 博客

主题设计 Anders Noren返回顶部 ↑