[Python导入系统] 当`import os.path`时,导入系统发生了什么?

由Jeza Chen 发表于 September 28, 2024

前言

在编写上一篇文章时,又发现了一个很有趣的地方: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模块中的一个属性,其实际上是posixpathntpath模块(视操作系统而定)的别名。此外,它还会将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,此时parentos

然后,Python会检查sys.modules中是否已经存在os模块,如果不存在,则先会调用_call_with_frames_removed函数尝试导入os模块。在os.py中,我们已经看到,os模块执行的过程中会根据操作系统类型导入posixpathntpath模块,并将其赋值给os.path属性。因此,当os模块导入完成后,os.path属性已经存在,与此同时sys.modules中已经有了os.path,因此_find_and_load_unlocked函数会直接返回sys.modules['os.path'],而不会继续执行后面的导入流程!

怪不得会有个Cracy side-effects!的注释,哈哈~