比尔盖子 博客

为什么 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 的独立模块。根据同样的原理,我们不难在自己的项目中使用同样的技巧。

Categories: Python

服务器维护初步结束 » « 开发者张峻锋被指参与恶意软件

4 Comments

  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 任意你已安装的二进制程序名

发表评论

Your email address will not be published.

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax

Copyright © 2023 比尔盖子 博客

Theme by Anders NorenUp ↑