[英]What is the best practice for imports when developing a Python package?
我正在尝试构建一个 Python package,其中包含子模块和子包(“库”)。 我到处都在寻找正确的方法,但令人惊讶的是我发现它非常复杂。 当然也经历了 StackOverFlow 中的多个线程..
问题如下:
为了从另一个目录导入模块或 package,在我看来有 2 个选项:a。 将绝对路径添加到sys.path
。 b. 使用setuptools.setup
function 在 package 的主目录中的setup.py
文件中安装 package - 将 package 安装到正在使用的特定 Python 版本的site-packages
目录中。
选项a对我来说似乎太笨拙了。 选项b很棒,但我发现它不切实际,因为我目前正在工作和编辑包的源代码 - 当然,更改不会在 package 的安装目录上更新。 另外package的安装目录没有被Git跟踪,不用说我用的是Git原目录。
总结这个问题:从当前正在建设中的 Python package 的子目录中自由且良好地导入模块和子包的最佳实践是什么?
我觉得我遗漏了一些东西,但到目前为止找不到合适的解决方案。
谢谢!
这是一个很好的问题,我希望更多的人能按照这些思路思考。 在其他人可以轻松使用之前,绝对有必要使模块可导入并最终可安装。
在我回答之前,我会说当我对现有 package 结构之外的文件进行初始开发时,我确实使用了 sys.path munging。 我有一个编辑器片段可以构造如下代码:
import sys, os
sys.path.append(os.path.expanduser('~/path/to/parent'))
from module_of_interest import * # NOQA
给定我使用的当前文件的路径:
import ubelt as ub
fpath = ub.Path('/home/username/path/to/parent/module_of_interest.py')
modpath, modname = ub.split_modpath(fpath, check=False)
modpath = ub.Path(modpath).shrinkuser() # abstract home directory
为了构建必要的部分,代码片段将插入到文件中,这样我就可以在 IPython 中与它进行交互。 我发现花了一些额外的时间来删除对我的显式主文件夹的引用,这样只要用户具有与主目录相同的相对路径结构,代码仍然可以工作,这使得它稍微更便携。
话虽这么说, sys.path munging 不是一个可持续的解决方案。 最终,您希望您的 package 由 python package 经理管理。 我知道很多人使用诗歌,但我喜欢普通的旧 pip,所以我可以描述这个过程,但知道这不是唯一的方法。
为此,我们需要了解一些基础知识 go。
你必须知道你在什么 Python 环境中工作。理想情况下,这是一个用pyenv (或conda或 mamba 或 poetry ...)管理的虚拟环境。 但也可以在您的全局系统 Python 环境中执行此操作,但不建议这样做。 我喜欢在 my.bashrc 中始终激活的单一默认 Python 虚拟环境中工作。 它总是很容易切换到一个新的或吹走它/重新开始。
您需要考虑两个根路径:您的存储库的根目录,我将其称为您的 repo 路径,以及您的根目录到您的 package,package 路径或模块路径,它应该是一个名称为顶级 Python 的文件夹package。您将使用此名称导入它。 此 package 路径必须位于回购路径内。 一些 repos,比如xdoctest ,喜欢将模块路径放在src
目录中。 其他人,如ubelt ,喜欢在存储库的顶层拥有 repo 路径。 我认为第二种情况对于新的 package 创建者/维护者来说在概念上更容易,所以让我们 go 吧。
所以现在,你处于一个激活的 Python 虚拟环境中,我们已经指定了一个路径,我们将检查 repo。 我喜欢在$HOME/code
中克隆 repos,所以也许 repo 路径是$HOME/code/my_project
。
在此回购路径中,您应该有根路径 package。 假设您的 package 名为 mypymod。 任何包含__init__.py
文件的目录在概念上都是一个 python 模块,其中__init__.py
的内容是您导入该目录名称时获得的内容。 目录模块和普通文件模块之间的唯一区别是目录模块/包可以有子模块或子包。
例如,如果你在my_project
mypymod
ls
,你有一个看起来像这样的文件结构......
+ my_project
+ mypymod
+ __init__.py
+ submod1.py
+ subpkg
+ __init__.py
+ submod2.py
您可以导入以下模块:
import mypymod
import mypymod.submod1
import mypymod.subpkg
import mypymod.subpkg.submod2
如果您确保当前工作目录始终是存储库根目录,或者将存储库根目录放入sys.path
,那么这就是您所需要的。 在sys.path
或 CWD 中可见是另一个模块可以看到您的模块所需要的。
现在的诀窍是:你如何确保你的其他包/脚本总能看到这个模块? 这就是 package 经理的用武之地。为此,我们需要一个setup.py
或更新的pyproject.toml
变体。 我将描述旧的setup.py
做事方式。
您需要做的就是将setup.py
放在您的repo root中。 注意:它不在您的 package 目录中的 go。 有很多关于如何编写 setup.py的资源,所以我不会详细描述它,但基本上您需要的只是用足够的信息填充它,以便它知道 package 的名称、它的位置和它的版本.
from setuptools import setup
setup(
name='mypymod',
version='0.1.0',
packages=find_packages(include=['mypymod', 'mypymod.*']),
install_requires=[],
)
所以你的 package 结构将如下所示:
+ my_project
+ setup.py
+ mypymod
+ __init__.py
+ submod1.py
+ subpkg
+ __init__.py
+ submod2.py
您可以指定很多其他内容,我建议您查看 ubelt 和 xdoctest 作为示例。 我会注意到它们包含从requirements.txt
或requirements/*.txt
文件中解析需求的非标准方式,我认为这通常比人们处理需求的标准方式要好。 但我离题了。
给定pip
或其他一些 package 管理器(例如 pipx、poetry)识别为package 清单的内容 - 一个描述 package 内容的文件,您现在可以安装它。 如果您仍在开发它,您可以在可编辑模式下安装它,而不是将 package 复制到您的站点包中,只创建一个符号链接,因此每次调用 Python(或立即调用)时,代码中的任何更改都会反映出来如果你使用 IPython 自动重新加载)。
使用 pip 就像运行pip install -e <path-to-repo-root>
一样简单,这通常通过导航到 repo 并运行pip install -e.
.
恭喜,您现在有一个 package 可以从任何地方参考。
现在你有了一个 package,你可以引用它,就好像它是通过 pip 从 pypi 安装的一样。 有一些技巧可以有效地使用它。 第一个是运行脚本。
在 Python 中,您无需指定文件路径即可将其作为脚本运行。可以仅使用其模块名称将脚本作为__main__
运行。 这是通过 Python 的-m
参数完成的。例如,您可以运行python -m mypymod.submod1
它将调用$HOME/code/my_project/mypymod/submod1.py
作为主模块(即它的__name__
属性将被设置为"__main__"
)。
此外,如果您想使用目录模块执行此操作,您可以在该目录中创建一个名为__main__.py
的特殊文件,这就是将要执行的脚本。 例如,如果我们修改我们的 package 结构
+ my_project
+ setup.py
+ mypymod
+ __init__.py
+ __main__.py
+ submod1.py
+ subpkg
+ __init__.py
+ __main__.py
+ submod2.py
现在python -m mypymod
将执行$HOME/code/my_project/mypymod/__main__.py
和python -m mypymod.subpkg
将执行$HOME/code/my_project/mypymod/subpkg/__main__.py
。 这是使模块兼作可导入 package 和命令行可执行文件(例如 xdoctest 执行此操作)的一种非常方便的方法。
您可能会注意到的一个痛点是,在上面的代码中,如果您运行:
import mypymod
mypymod.submod1
你会得到一个错误,因为默认情况下 package 在导入之前不知道它的子模块。 您需要填充__init__.py
以公开您希望在顶层访问的任何属性。 您可以使用以下内容填充mypymod/__init__.py
:
from mypymod import submod1
现在上面的代码可以工作了。
不过这有一个折衷。 您立即访问的东西越多,导入模块所需的时间就越多,而且对于大包来说,它会变得相当麻烦。 此外,您还必须手动编写代码来公开您想要的内容,所以如果您想要一切,那将是一件痛苦的事情。
如果你看一下ubelt 的init .py ,你会发现它有大量代码明确地使每个子模块中的每个 function 都可以在顶层访问。 我已经编写了另一个名为mkinit的库,它实际上自动执行了这个过程,它还可以选择使用lazy_loader库来减轻在顶层公开所有属性对性能的影响。 我发现 mkinit 工具在编写大型嵌套包时非常有用。
总结以上内容:
__init__.py
。mkinit
自动生成__init__.py
文件的内容。setup.py
/ pyproject.toml
放在“回购路径”的根目录中。pip install -e.
在开发时以可编辑模式安装 package。python -m
将模块名称作为脚本调用。希望这可以帮助。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.