即使其中一些操作 sys.path,PYTHONPATH 在多个导入语句中是否一致?

Is PYTHONPATH consistent across multiple import statements even if some of them manipulate the sys.path?

PYTHONPATH 文档 https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH 说 "The search path can be manipulated from within a Python program as the variable sys.path."

即另一个模块可以自由编辑 sys.path 并将其附加到列表的任何位置,甚至可以将其清空。

我的理解是,为了保持一致的搜索顺序,使用了 PYTHONPATH,不是吗?

让我们假设 "y" 模块在脚本中更改 sys.path A.py 导入 x 导入y 导入 z

  1. Python 解释器看到 PYTHONPATH 并且 sys.path 被解释器更新,然后导入 x
  2. import y 将新路径附加到 sys.path 列表的开头或结尾,这意味着它也将导入 sys 模块。
  3. 现在,由于 sys.path 已在 y 中更改,根据导入语句的工作方式 https://docs.python.org/3.7/library/sys.html#sys.modules 我的理解是 sys.path 会永久更改,直到解释器关闭。也许在 A.py 中再次重新加载 sys 模块会重置 sys.path 以使用 PYTHONPATH 搜索顺序?

我希望在顶层模块中有一个一致的搜索顺序路径,我相信它可能会受到另一个子 module/imports 更改它的影响。 PYTHONPATH 是获得这个的方法还是有一些我还不知道的其他 tip/trick?

如果您的模块 y 更改 sys.path,即使您执行 importlib.reload(sys)

,您的 A.py 脚本中的值也会相同

所以假设模块 'y' 执行

from sys import path
path.clear()

在您的 A.py 脚本中:

import sys, importlib
import x, y

importlib.reload(sys)
print(sys.path) # is []

import z

找不到模块 z。

要解决此问题,您可以将脚本 sys.path 变量恢复为解释器开始时指定的相同值。

来自文档:

A list of strings that specifies the search path for modules. Initialized from the environment variable PYTHONPATH, plus an installation-dependent default.

还有……

As initialized upon program startup, the first item of this list, path[0], is the directory containing the script that was used to invoke the Python interpreter

假设解释器不在交互模式下 运行 或从 stdin 读取(它正在执行文件脚本)并且它位于当前工作目录
我们的 A.py 可能看起来像:

import importlib

import x, y

# We can still load (sys, os, ...)
from sys import path
from os import getcwd
import site

print(sys.path) # []

path.append(getcwd()) # Add directory where script is executed
path.append(os.environ.get('PYTHONPATH')) # Add PYTHONPATH
site.main() # Add site packages

import z # Now this dont fail

注意:即使删除所有 sys.path 项,importlib 也能够找到包 ossitesys、...

这是因为 importlib 使用 sys.modules 访问这样的包:

来自 importlib.find_loader 文档:

If the module is in sys.modules, then sys.modules[name].loader is returned

并且来自 sys.modules 文档:

This is a dictionary that maps module names to modules which have already been loaded.


编辑:
这是一个可用于解决此问题的棘手解决方案:您可以创建一个函数,每次加载模块时都会调用该函数。该函数检查模块加载后 sys.path 是否被更改。 如果为真,将其设置为原始值

from copy import copy
import warnings
import sys

sys.path = list(sys.path)
_original_path = copy(sys.path)
_base_import = __import__

def _import(*args, **kwargs):
    try:
        module = _base_import(*args, **kwargs)
        return module
    finally:
        if type(sys.path) != list or sys.path != _original_path:
            warnings.warn('System path was modified', Warning)
            # Restore path
            sys.path = copy(_original_path)

__builtins__.__import__ = _import

现在执行这段代码:

import sys

before = copy(sys.path)
import y # 'y' tries to change sys.path
after = copy(sys.path)

print(before == after) # True 

它还会在标准输出上显示一条警告消息


编辑#2(另一种解决方案):
这仅适用于 python >=3.7 因为它依赖于 PEP 562
这里我基本上替换了模块 'sys' 这样我就可以避免外部模块改变实际的 sys.path

首先用下一个代码创建一个脚本(proxy.py):

import importlib
from sys import path, modules
from copy import copy

path = copy(path)
modules = copy(modules)

def __getattr__(name):
    if name in globals():
        return getattr(globals(), name)
    return getattr(importlib.import_module('sys'), name)

def __dir__():
    return dir(importlib.import_module('sys'))

现在,在您的 A.py 上输入下一个代码:

import proxy
import sys
sys.modules['sys'] = proxy

import y # y imports 'sys' but import sys returns the 'proxy' module
# 'y' thinks he changes sys.path but it only modifies  proxy.path

print(proxy.path) # []
print(sys.path) # Unchanged

y 模块上的代码:

import sys
sys.path.clear() # a.k: proxy.path.clear()

# You can still access to all properties from the sys module
print(dir(sys)) # ['ps1', 'ps2', 'platform', ...]