即使其中一些操作sys.path,PYTHONPATH是否在多个import语句之间保持一致?

时间:2019-07-06 08:58:27

标签: python pythonpath

PYTHONPATH文档https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH说:“可以从Python程序中将搜索路径作为变量sys.path进行操作。”

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

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

让我们假设“ y”模块在脚本A.py中更改sys.path 汇入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搜索顺序吗?

我希望在顶层模块中具有一致的搜索顺序路径,我认为这可能会受到另一个子模块/导入对其进行更改的影响。 PYTHONPATH是获取此信息的方法还是我还不知道的其他提示/技巧?

1 个答案:

答案 0 :(得分:0)

如果您的模块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变量恢复为解释器开头指定的值。

从文档中:

  

一个字符串列表,它指定模块的搜索路径。从环境变量PYTHONPATH初始化,再加上依赖于安装的默认值。

还有...

  

在程序启动时初始化,此列表的第一项path [0]是包含用于调用Python解释器的脚本的目录

让我们假设解释器未在交互模式下运行或未从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文档:

  

如果模块在sys.modules中,则返回sys.modules [name]。 loader

根据sys.modules文档:

  

这是一个字典,将模块名称映射到已加载的模块。


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

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 

它还将在标准输出上显示警告消息


EDIT#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', ...]