今天在微博上,@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
确实被执行了一次。而且,os
和 os.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.path
到 sys.modules
里,然后,Python 才尝试导入 os.path
,这时它 已经位于导入列表,于是得以顺利导入。因此,与其把 os.path
看作 os
的子模块,不如把它看作名为 os.path
的独立模块。根据同样的原理,我们不难在自己的项目中使用同样的技巧。
2017年01月29日 — 00:29
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.
2017年01月20日 — 10:27
I was ablᥱ to find good info ffom your blog articles.
2015年08月20日 — 01:09
sh.py 更有意思,你可以 from sh import 任意你已安装的二进制程序名
2015年08月20日 — 02:59
我用它写过不少运维脚本呢 😀