Python 3.6项目结构导致RuntimeWarning

Mat*_*hew 28 python python-3.x

我正在尝试打包我的项目以进行分发,但是RuntimeWarning当我运行模块时我正在尝试.

我在Python邮件列表上找到了一个错误报告,表明这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.`
Run Code Online (Sandbox Code Playgroud)

运行我的测试很好:

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

OK
Run Code Online (Sandbox Code Playgroud)

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

myproject/
    proj/
        __init__.py
        proj.py
    tests/
        __init__.py
        context.py
        test_proj.py
Run Code Online (Sandbox Code Playgroud)

在文件中proj/proj.py:

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

if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)

proj/__init__.py:

from .proj import main
Run Code Online (Sandbox Code Playgroud)

tests/context.py:

import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import proj
Run Code Online (Sandbox Code Playgroud)

最后,在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()
Run Code Online (Sandbox Code Playgroud)

任何人都可以帮我纠正我的项目结构,以避免这种双重导入的情况吗?任何有关这方面的帮助将不胜感激.

nco*_*lan 30

对于这种特殊情况,双重导入警告是由于以下行proj/__init__.py:

from .proj import main
Run Code Online (Sandbox Code Playgroud)

什么线装置是由时-m开关执行结束import proj步骤,proj.proj已经被导入作为进口父包的副作用.

避免警告

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

解决这两个问题的两个主要选择是:

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

    from .proj import main
    main()
    
    Run Code Online (Sandbox Code Playgroud)

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

选项2的更向后兼容的变体是添加__main__.py而不从当前子模块中删除CLI块,并且当与DeprecationWarning以下组合时,这可能是一种特别好的方法:

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

如果proj/__main__.py已经被用于其他目的,那么你也可以像换东西python -m proj.projpython -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()
Run Code Online (Sandbox Code Playgroud)

为什么警告存在?

当此警告时发出-m开关实现约去运行已导入模块的代码再次__main__类,函数,容器等-模块,这意味着你将它定义了一切的两种截然不同的副本

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

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

如何调试更复杂的案例?

虽然在这个特定的例子中,副作用导入是直接进入的proj/__init__.py,但是有一个更加微妙且难以调试的变体,而父包改为:

import some_other_module
Run Code Online (Sandbox Code Playgroud)

然后是some_other_module(或它导入的模块):

import proj.proj # or "from proj import proj"
Run Code Online (Sandbox Code Playgroud)

假设错误行为是可重现的,调试这些问题的主要方法是以详细模式运行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
Run Code Online (Sandbox Code Playgroud)

这个特殊的例子只显示Fedora上的Python 2.7在启动时所做的基本导入集.当调试RuntimeWarning像本问题中那样的双重导入时,你将在详细输出中搜索"import proj"然后"import proj.proj"行,然后仔细查看"import"之前的导入proj.proj"line.

  • 感谢您添加此答案.因此,为了提炼它,消息是:当文件也自动导入包中时,你不能将文件作为可执行模块(`python -m WHATEVER.FILE`)运行(任何`__init __.py`,这是loaded,包含`from WHATEVER import FILE`).对? (2认同)

Joh*_*fis 9

如果你看一下双重导入陷阱,你会看到:

下一个陷阱存在于所有当前版本的Python中,包括3.3,并且可以在以下一般准则中总结:"永远不要将包目录或包内的任何目录直接添加到Python路径".

这有问题的原因是该目录中的每个模块现在都可以在两个不同的名称下访问:作为顶级模块(因为目录在sys.path上)和作为包的子模块(如果包含更高级别的目录)包本身也在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
    
    Run Code Online (Sandbox Code Playgroud)

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


小智 8

@ncoghlan 答案是正确的。我只想添加到他的解决方案 1 中,__init__.py如果您使用开关执行包,则只需删除导入即可-m。归根结底就是要弄清楚__init__.pypython 是否是通过-mswitch 调用的。sys.flags不幸的是,不包含开关的条目-m,但sys.argv似乎包含一个包含“-m”的元素(但是,我没有弄清楚这种行为是否属实)。__init__.py所以按如下方式改变:

import sys
if not '-m' in sys.argv:
    from .proj import main
Run Code Online (Sandbox Code Playgroud)

-m如果您使用开关执行包,则.proj不会导入该包__init__.py,从而避免了双重导入。如果从另一个脚本导入包,.proj则会按预期导入。不幸的是,sys.argv不包含开关的参数-m!因此,也许将 main() 函数移至单独的文件是更好的解决方案。但我真的很喜欢在我的模块中有一个 main() 函数,以进行快速简单的测试/演示。