Python3.6项目结构导致运行时警告

2024-04-22 18:06:32 发布

您现在位置:Python中文网/ 问答频道 /正文

我正试图打包我的项目以供分发,但在运行模块时遇到了RuntimeWarning

我在Python mailing list上发现了一个错误报告,它表明RuntimeWarning是Python 3.5.2中引入的新行为。

在阅读错误报告时,似乎出现了一个双重导入,并且这个RuntimeWarning在警告用户时是正确的。但是,我不知道为了避免这个问题,我需要对自己的项目结构做什么改变。

这是我试图“正确”构建的第一个项目。我想有一个整洁的布局,当我推的代码,和一个项目结构,可以克隆和运行的其他人容易。

我的结构主要基于http://docs.python-guide.org/en/latest/writing/structure/

我在下面添加了一个最小工作示例的详细信息。

为了复制这个问题,我使用python -m运行主文件:

(py36) X:\test_proj>python -m proj.proj
C:\Users\Matthew\Anaconda\envs\py36\lib\runpy.py:125: RuntimeWarning: 
'proj.proj' found in sys.modules after import of package 'proj', but prior 
to execution of 'proj.proj'; this may result in unpredictable behaviour
  warn(RuntimeWarning(msg))
This is a test project.`

运行我的测试很好:

(py36) X:\test_proj>python -m unittest tests.test_proj
This is a test project.
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

复制该问题的项目结构如下:

myproject/
    proj/
        __init__.py
        proj.py
    tests/
        __init__.py
        context.py
        test_proj.py

在文件proj/proj.py中:

def main():
    print('This is a test project.')
    raise ValueError

if __name__ == '__main__':
    main()

proj/__init__.py中:

from .proj import main

tests/context.py中:

import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import proj

最后,在tests/test_proj.py中:

import unittest

from .context import proj


class SampleTestCase(unittest.TestCase):
    """Test case for this sample project"""
    def test_raise_error(self):
        """Test that we correctly raise an error."""
        with self.assertRaises(ValueError):
            proj.main()


if __name__ == '__main__':
    unittest.main()

有谁能帮我纠正我的项目结构,以避免这种双重导入的情况?如果有任何帮助,我们将不胜感激。


Tags: path项目inpytestimportprojectos
3条回答

对于这种特殊情况,双重导入警告是由于proj/__init__.py中的这一行引起的:

from .proj import main

这一行的意思是,当-m开关实现完成import proj步骤时,proj.proj已经导入了作为导入父包的副作用。

避免警告

为了避免出现警告,您需要找到一种方法来确保导入父包不会隐式导入使用-m开关执行的包。

解决的两个主要选项是:

  1. 删除from .proj import main行(正如@John Moutafis建议的那样),假设这可以在不破坏API兼容性保证的情况下完成;或者
  2. proj子模块中删除if __name__ == "__main__":块,并将其替换为一个单独的proj/__main__.py文件,该文件只执行以下操作:

    from .proj import main
    main()
    

如果使用选项2,那么命令行调用也将更改为仅python -m proj,而不是引用子模块。

选项2的一个更向后兼容的变体是在不从当前子模块中删除CLI块的情况下添加__main__.py,当与DeprecationWarning结合使用时,这是一个特别好的方法:

if __name__ == "__main__":
    import warnings
    warnings.warn("use 'python -m proj', not 'python -m proj.proj'", DeprecationWarning)
    main()

如果proj/__main__.py已经用于其他目的,那么您还可以将python -m proj.proj替换为python -m proj.proj_cli,其中proj/proj_cli.py看起来像:

if __name__ != "__main__":
    raise RuntimeError("Only for use with the -m switch, not as a Python API")
from .proj import main
main()

为什么存在警告?

-m开关实现即将开始并在__main__模块中再次运行已导入模块的代码时,会发出此警告,这意味着您将拥有它定义的所有内容的两个不同副本-类、函数、容器等

根据应用程序的具体情况,这可能工作正常(这就是为什么它是一个警告而不是一个错误),或者它可能导致奇怪的行为,如模块级的状态修改未按预期共享,或者甚至由于异常处理程序试图从一个实例捕获异常类型而未捕获异常而引发的异常使用了另一个实例的类型。

因此出现了模糊的this may cause unpredictable behaviour警告-如果由于运行模块的顶级代码两次而导致出现问题,那么症状可能是任何事情。

如何调试更复杂的案例?

虽然在这个特定的示例中,副作用导入直接在proj/__init__.py中,但是有一个更微妙、更难调试的变量,父包会执行以下操作:

import some_other_module

然后是some_other_module(或它导入的模块)执行以下操作:

import proj.proj # or "from proj import proj"

假设错误行为是可复制的,调试此类问题的主要方法是以详细模式运行python并检查导入顺序:

$ python -v -c "print('Hello')" 2>&1 | grep '^import'
import zipimport # builtin
import site # precompiled from /usr/lib64/python2.7/site.pyc
import os # precompiled from /usr/lib64/python2.7/os.pyc
import errno # builtin
import posix # builtin
import posixpath # precompiled from /usr/lib64/python2.7/posixpath.pyc
import stat # precompiled from /usr/lib64/python2.7/stat.pyc
import genericpath # precompiled from /usr/lib64/python2.7/genericpath.pyc
import warnings # precompiled from /usr/lib64/python2.7/warnings.pyc
import linecache # precompiled from /usr/lib64/python2.7/linecache.pyc
import types # precompiled from /usr/lib64/python2.7/types.pyc
import UserDict # precompiled from /usr/lib64/python2.7/UserDict.pyc
import _abcoll # precompiled from /usr/lib64/python2.7/_abcoll.pyc
import abc # precompiled from /usr/lib64/python2.7/abc.pyc
import _weakrefset # precompiled from /usr/lib64/python2.7/_weakrefset.pyc
import _weakref # builtin
import copy_reg # precompiled from /usr/lib64/python2.7/copy_reg.pyc
import traceback # precompiled from /usr/lib64/python2.7/traceback.pyc
import sysconfig # precompiled from /usr/lib64/python2.7/sysconfig.pyc
import re # precompiled from /usr/lib64/python2.7/re.pyc
import sre_compile # precompiled from /usr/lib64/python2.7/sre_compile.pyc
import _sre # builtin
import sre_parse # precompiled from /usr/lib64/python2.7/sre_parse.pyc
import sre_constants # precompiled from /usr/lib64/python2.7/sre_constants.pyc
import _locale # dynamically loaded from /usr/lib64/python2.7/lib-dynload/_localemodule.so
import _sysconfigdata # precompiled from /usr/lib64/python2.7/_sysconfigdata.pyc
import abrt_exception_handler # precompiled from /usr/lib64/python2.7/site-packages/abrt_exception_handler.pyc
import encodings # directory /usr/lib64/python2.7/encodings
import encodings # precompiled from /usr/lib64/python2.7/encodings/__init__.pyc
import codecs # precompiled from /usr/lib64/python2.7/codecs.pyc
import _codecs # builtin
import encodings.aliases # precompiled from /usr/lib64/python2.7/encodings/aliases.pyc
import encodings.utf_8 # precompiled from /usr/lib64/python2.7/encodings/utf_8.pyc

这个特定的例子只显示了启动时Fedora上Python2.7所做的基本导入集。在调试与本问题类似的双导入RuntimeWarning时,您将在详细输出中搜索“import proj”和“import proj.proj”行,然后仔细查看“import proj.proj”行之前的导入。

python-m有点棘手。@ncoghlan已经提供了详细的信息。 当我们尝试使用python-m运行时,默认情况下sys.path/pythonpath中的所有包都被导入。如果您的包对路径中目录中的任何内容都有import语句,则会出现上述警告。See the Pic

我的PYTHONPATH已经有了项目目录。所以当我这样做的时候

from reader.reader import Reader

系统抛出警告。因此,如果路径在python path中,则不需要显式导入

如果你看一下double import trap,你会看到:

This next trap exists in all current versions of Python, including 3.3, and can be summed up in the following general guideline: “Never add a package directory, or any directory inside a package, directly to the Python path”.

The reason this is problematic is that every module in that directory is now potentially accessible under two different names: as a top level module (since the directory is on sys.path) and as a submodule of the package (if the higher level directory containing the package itself is also on sys.path).

tests/context.py

删除:sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

这可能会导致问题,并且您的代码仍然按预期工作。


根据评论编辑:

您可以尝试更改代码中的某些部分:

  1. proj/__init__.py可以完全为空
  2. test_proj.py上,应按如下方式更改导入:

    import unittest
    
    from proj import proj
    

PS:我无法在Linux上用您的初始代码或我的建议重现警告。

相关问题 更多 >