背景
我有一个Python应用程序依赖于另一个包作为git子模块提供,产生类似于以下的目录结构:
foo/
bar/
bar/
__init__.py
eggs.py
test/
setup.py
foo/
__init__.py
ham.py
main.py
访问foo
包很简单,因为main.py
是从顶层foo/
目录执行的;但bar
包嵌套在另一个bar
目录中,不能直接导入。
通过在sys.path
开头修改main.py
,这很容易解决:
import sys
# Or sys.path.append()
sys.path.insert(0, './bar')
from bar.eggs import Eggs
from foo.ham import Ham
(注意:这个代码示例假设main.py
将始终从foo/
调用;在可能不是这种情况的情况下,'.bar'
可以用os.path.join(os.path.dirname(__file__), 'bar')
替换,尽管这显然更加笨拙。)
问题
不幸的是,pylint并不喜欢这种解决方案。当代码工作时,linter认为sys.path
修改是一个结束“模块顶部”的代码块,并给出了一个不受欢迎的wrong-import-position
警告:
C: 6, 0: Import "from bar.eggs import Eggs" should be placed at the top of the module (wrong-import-position)
C: 7, 0: Import "from foo.ham import Ham" should be placed at the top of the module (wrong-import-position)
类似的问题
Adding a path to sys.path in python and pylint
这个提问者有一个问题,pylint无法正确解析导入。这个问题的唯一答案表明增加了pylint的内部路径;这没有什么可以避免关于交错的sys.path
修改的投诉。
配置pylint
禁用wrong-import-position
中的.pylintrc
检查程序是最简单的解决方案,但会抛弃有效的警告。
一个更好的解决方案是告诉pylint忽略这些导入的wrong-import-position
,内联。误报导入可以嵌套在启用禁用块中,而不会丢失任何其他地方的覆盖:
import sys
sys.path.insert(0, './bar')
#pylint: disable=wrong-import-position
from bar.eggs import Eggs
from foo.ham import Ham
#pylint: enable=wrong-import-position
Ham()
# Still caught
import something_else
然而,如果在wrong-import-order
中.pylintrc
被禁用,这确实会有轻微的下滑。
避免修改sys.path
有时不需要的掉毛警告源于错误地开始出现问题。我想出了一些方法来避免首先修改sys.path
,尽管它们不适用于我自己的情况。
也许最直接的方法是修改PYTHONPATH
以包含子模块目录。但是,每次调用应用程序或在系统/用户级别修改应用程序时都必须指定这一点,这可能会损害其他进程。该变量可以在包装shell或批处理脚本中设置,但这需要进一步的环境假设或限制Python调用的更改。
更现代,更少麻烦的模拟是在虚拟环境中安装应用程序,只需将子模块路径添加到虚拟环境即可。
到达更远的地方,如果子模块包含setuptools setup.py
,它可以简单地安装,完全避免路径定制。这可以通过将出版物维护到诸如pypi(专有包的非启动程序)之类的存储库,或者通过利用/滥用pip install -e
来直接安装子模块包或从其存储库来安装。虚拟环境再一次避免了潜在的跨应用程序冲突和权限问题,从而使此解决方案更加简单。
如果目标操作系统集可以限制为具有强大符号链接支持的操作系统(实际上这排除了所有Windows至少10个),则子模块可以链接到绕过包装目录并将目标包直接放在工作目录中:
foo/
bar/ --> bar_src/bar
bar_src/
bar/
__init__.py
eggs.py
test/
setup.py
foo/
__init__.py
ham.py
main.py
这有限制了应用程序的潜在用户和填充foo
目录的混乱,但在某些情况下可能是一个可接受的解决方案。
这种设置的问题在于它对文件的位置做出了非常具体的假设。特别是,它将位置硬编码到另一个包。
在您的情况下,您将其硬编码为相对路径。这另外要求最终用户具有非常特定的当前目录。如果您是最终用户,这很烦人。如果我有一个文件我想用作你的代码的输入,我应该能够将我当前的目录作为我的用户主目录路径(Linux中的~
,Windows中的%USERPRPOFILE%
)并在使用时传递我的文件的相对路径脚本本身的绝对路径。 (例如,python /path/to/your/script ./myinput.txt
。)像这样的硬编码位置使其无法做到。我还注意到你的bar
目录包含一个setup.py
,暗示它是一个独立的包。精彩。如果我想再次运行main.py
某个特定版本的软件包怎么办?同样,对于脚本执行的sys.path
修改,这是不可能的。
您应该在代码中进行硬编码的唯一位置是资源的位置,这些资源将直接与代码一起分发,始终位于同一位置,就像您在recipes.dat
旁边有eggs.py
文件一样。在这种情况下,路径应该相对于脚本(或其他语言中的二进制文件)的当前位置。 (例如,RECIPES_PATH = os.path.join(os.path.dirname(__name__), 'recipes.dat')
。)当你有一个单独的包时,它可能在你的main.py
脚本所期望的其他地方。
查找和加载包是Python的基本功能。让它做到这一点。当您遇到无法立即找到它的情况时(因为您的代码未安装在任何地方),请使用标准机制来处理它们。
PYTHONPATH
环境变量可能是处理它的最简单方法。这很容易。您只需要一个配套脚本来设置命令行环境:
setupenv.sh
:
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # See https://stackoverflow.com/a/246128/1394393
if [ -n "$PYTHONPATH" ]; then
PYTHONPATH=$PYTHONPATH:
fi
PYTHONPATH=$PYTHONPATH${DIR%%/}/bar
然后:
$ source setupenv.sh
$ python ./main.py
(在Windows批处理/ cmd文件中执行此操作也同样简单。)
好的,在您积极开发代码时,每次启动终端时都必须设置环境,这有点烦人。但它并没有那么糟糕。我在自己的项目中这样做,这是我早上做的事情,在我推出新终端之前不要再考虑了。 (我的脚本设置了更多:激活虚拟环境,为一些原生二进制文件设置PATH
。)对于项目来说,它更加清洁。
你可能会争辩说,“好吧,我们仍然在对sh文件中的位置进行硬编码。”是的我们是。但是这个脚本是存储库的一部分。请注意,我使用的路径是相对于脚本本身;那是因为我知道代码库是如何构建的。当他们在命令行工作时我不知道用户的当前目录,我当然不知道main.py
将在哪里发布。也许它最终将在最终目的地的自己的包装中。无论如何,知道其他软件包所在的位置并不是该脚本的工作。这是此setupenv.sh
脚本的工作,在此存储库中。