繁体   English   中英

开发 Python package 时导入的最佳实践是什么?

[英]What is the best practice for imports when developing a Python package?

我正在尝试构建一个 Python package,其中包含子模块和子包(“库”)。 我到处都在寻找正确的方法,但令人惊讶的是我发现它非常复杂。 当然也经历了 StackOverFlow 中的多个线程..

问题如下:

  1. 为了从另一个目录导入模块或 package,在我看来有 2 个选项:a。 将绝对路径添加到sys.path b. 使用setuptools.setup function 在 package 的主目录中的setup.py文件中安装 package - 将 package 安装到正在使用的特定 Python 版本的site-packages目录中。

  2. 选项a对我来说似乎太笨拙了。 选项b很棒,但我发现它不切实际,因为我目前正在工作和编辑包的源代码 - 当然,更改不会在 package 的安装目录上更新。 另外package的安装目录没有被Git跟踪,不用说我用的是Git原目录。

总结这个问题:从当前正在建设中的 Python package 的子目录中自由且良好地导入模块和子包的最佳实践是什么?

我觉得我遗漏了一些东西,但到目前为止找不到合适的解决方案。

谢谢!

这是一个很好的问题,我希望更多的人能按照这些思路思考。 在其他人可以轻松使用之前,绝对有必要使模块可导入并最终可安装。

在 sys.path 修改上

在我回答之前,我会说当我对现有 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 中与它进行交互。 我发现花了一些额外的时间来删除对我的显式主文件夹的引用,这样只要用户具有与主目录相同的相对路径结构,代码仍然可以工作,这使得它稍微更便携。

妥善管理 Python Package

话虽这么说, sys.path munging 不是一个可持续的解决方案。 最终,您希望您的 package 由 python package 经理管理。 我知道很多人使用诗歌,但我喜欢普通的旧 pip,所以我可以描述这个过程,但知道这不是唯一的方法。

为此,我们需要了解一些基础知识 go。

基本

  1. 你必须知道你在什么 Python 环境中工作。理想情况下,这是一个用pyenv (或conda或 mamba 或 poetry ...)管理的虚拟环境。 但也可以在您的全局系统 Python 环境中执行此操作,但不建议这样做。 我喜欢在 my.bashrc 中始终激活的单一默认 Python 虚拟环境中工作。 它总是很容易切换到一个新的或吹走它/重新开始。

  2. 您需要考虑两个根路径:您的存储库的根目录,我将其称为您的 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

现在的诀窍是:你如何确保你的其他包/脚本总能看到这个模块? 这就是 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.txtrequirements/*.txt文件中解析需求的非标准方式,我认为这通常比人们处理需求的标准方式要好。 但我离题了。

给定pip或其他一些 package 管理器(例如 pipx、poetry)识别为package 清单的内容 - 一个描述 package 内容的文件,您现在可以安装它。 如果您仍在开发它,您可以在可编辑模式下安装它,而不是将 package 复制到您的站点包中,只创建一个符号链接,因此每次调用 Python(或立即调用)时,代码中的任何更改都会反映出来如果你使用 IPython 自动重新加载)。

使用 pip 就像运行pip install -e <path-to-repo-root>一样简单,这通常通过导航到 repo 并运行pip install -e. .

恭喜,您现在有一个 package 可以从任何地方参考。

充分利用您的 package

python -m 调用

现在你有了一个 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__.pypython -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 工具在编写大型嵌套包时非常有用。

概括

总结以上内容:

  1. 确保你在 Python virtualenv 中工作(我推荐 pyenv)
  2. 在您的“回购路径”中识别您的“包路径”。
  3. 在每个你想成为 Python package 或子包的目录中放一个__init__.py
  4. 或者,使用mkinit自动生成__init__.py文件的内容。
  5. setup.py / pyproject.toml放在“回购路径”的根目录中。
  6. 使用pip install -e. 在开发时以可编辑模式安装 package。
  7. 使用python -m将模块名称作为脚本调用。

希望这可以帮助。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM