前言
在编写上一篇文章时,又发现了一个很有趣的地方:os
模块本身是一个py
文件,并非是一个包,但我们却可以通过import os.path
导入path
子模块,而在Python官方文档中,里面明确写道:
使用
import item.subitem.subsubitem
句法时,除最后一项外,每个item
都必须是包;最后一项可以是模块或包,但不能是上一项中定义的类、函数或变量。
而os
模块并不是一个包,那么import os.path
是如何实现导入的呢?
研究os.py
在标准库中找到os.py
文件,我们可以看到,os.py
文件中有如下代码:
# ...
if 'posix' in _names:
# ...
import posixpath as path
# ...
elif 'nt' in _names:
# ...
import ntpath as path
# ...
sys.modules['os.path'] = path
from os.path import (curdir, pardir, sep, pathsep, defpath, extsep, altsep,
devnull)
# ...
其实,os.path
并非是os
包(实际上也不是一个包…)的一个子模块,而是os
模块中的一个属性,其实际上是posixpath
或ntpath
模块(视操作系统而定)的别名。此外,它还会将os.path
添加到sys.modules
中。这进一步引起了疑问:如果没有事先通过improt os
执行os.py
,为什么也能直接通过import os.path
导入呢?我们进一步探究Python的导入流程。
研究importlib/_bootstrap.py
在Python的源码中,我们找到了importlib/_bootstrap.py
文件,其中有如下代码:
# ...
def _find_and_load_unlocked(name, import_):
path = None
parent = name.rpartition('.')[0]
parent_spec = None
if parent:
if parent not in sys.modules:
_call_with_frames_removed(import_, parent)
# Crazy side-effects!
if name in sys.modules:
return sys.modules[name]
parent_module = sys.modules[parent]
try:
path = parent_module.__path__
except AttributeError:
msg = f'{_ERR_MSG_PREFIX}{name!r}; {parent!r} is not a package'
raise ModuleNotFoundError(msg, name=name) from None
parent_spec = parent_module.__spec__
child = name.rpartition('.')[2]
# ...
# ...
可以看到,在_find_and_load_unlocked
函数中,当我们尝试导入os.path
时(此时name
参数为os.path
),会先尝试解析出父模块parent
,此时parent
为os
。
然后,Python会检查sys.modules
中是否已经存在os
模块,如果不存在,则先会调用_call_with_frames_removed
函数尝试导入os
模块。在os.py
中,我们已经看到,os
模块执行的过程中会根据操作系统类型导入posixpath
或ntpath
模块,并将其赋值给os.path
属性。因此,当os
模块导入完成后,os.path
属性已经存在,与此同时sys.modules
中已经有了os.path
,因此_find_and_load_unlocked
函数会直接返回sys.modules['os.path']
,而不会继续执行后面的导入流程!
怪不得会有个Cracy side-effects!
的注释,哈哈~