兄弟包导入
- 2024-12-05 08:37:00
- admin 原创
- 145
问题描述:
我尝试阅读有关兄弟导入的问题甚至
包文档,但我还没有找到答案。
结构如下:
├── LICENSE.md
├── README.md
├── api
│ ├── __init__.py
│ ├── api.py
│ └── api_key.py
├── examples
│ ├── __init__.py
│ ├── example_one.py
│ └── example_two.py
└── tests
│ ├── __init__.py
│ └── test_one.py
examples
和目录中的脚本如何 tests
从模块导入api
并从命令行运行?
另外,我想避免sys.path.insert
对每个文件进行丑陋的黑客攻击。这肯定可以用 Python 来完成,对吧?
解决方案 1:
厌倦了 sys.path 黑客吗?
有很多sys.path.append
可用的 hack,但我找到了一种解决手头问题的替代方法。
概括
将代码包装到一个文件夹中(例如
packaged_stuff
)创建
pyproject.toml
文件来描述你的包(见pyproject.toml
下面的最少内容)Pip 使用以下可编辑状态安装包
pip install -e <myproject_folder>
导入使用
from packaged_stuff.modulename import function_name
设置
起点是您提供的文件结构,包装在名为的文件夹中myproject
。
.
└── myproject
├── api
│ ├── api_key.py
│ ├── api.py
│ └── __init__.py
├── examples
│ ├── example_one.py
│ ├── example_two.py
│ └── __init__.py
├── LICENCE.md
├── README.md
└── tests
├── __init__.py
└── test_one.py
我将称之为.
根文件夹,在我的示例中,它位于C: mp est_imports
。
api.py
作为测试用例,我们使用以下内容./api/api.py
def function_from_api():
return 'I am the return value from api.api!'
测试一
from api.api import function_from_api
def test_function():
print(function_from_api())
if __name__ == '__main__':
test_function()
尝试运行test_one:
PS C: mp est_imports> python .myproject ests est_one.py
Traceback (most recent call last):
File ".myproject ests est_one.py", line 1, in <module>
from api.api import function_from_api
ModuleNotFoundError: No module named 'api'
尝试相对导入也不起作用:
使用from ..api.api import function_from_api
将导致
PS C: mp est_imports> python .myproject ests est_one.py
Traceback (most recent call last):
File ". ests est_one.py", line 1, in <module>
from ..api.api import function_from_api
ValueError: attempted relative import beyond top-level package
步骤
1)在根目录下创建一个 pyproject.toml 文件
(以前人们使用setup.py文件)
最小的内容pyproject.toml
将是*
[project]
name = "myproject"
version = "0.1.0"
description = "My small project"
[build-system]
build-backend = "flit_core.buildapi"
requires = ["flit_core >=3.2,<4"]
2)使用虚拟环境
如果您熟悉虚拟环境,请激活一个,然后跳到下一步。虚拟环境的使用不是绝对必要的,但从长远来看,它们确实会对您有所帮助(当您有多个正在进行的项目时……)。最基本的步骤是(在根文件夹中运行)
创建虚拟环境
python -m venv venv
激活虚拟环境
source ./venv/bin/activate
(Linux、macOS)或./venv/Scripts/activate
(Win)
要了解更多信息,只需在 Google 上搜索“python 虚拟环境教程”或类似内容。除了创建、激活和停用之外,您可能不需要任何其他命令。
创建并激活虚拟环境后,控制台应在括号中显示虚拟环境的名称
PS C: mp est_imports> python -m venv venv
PS C: mp est_imports> .envScriptsactivate
(venv) PS C: mp est_imports>
你的文件夹树应该看起来像这样**
.
├── myproject
│ ├── api
│ │ ├── api_key.py
│ │ ├── api.py
│ │ └── __init__.py
│ ├── examples
│ │ ├── example_one.py
│ │ ├── example_two.py
│ │ └── __init__.py
│ ├── LICENCE.md
│ ├── README.md
│ └── tests
│ ├── __init__.py
│ └── test_one.py
├── pyproject.toml
└── venv
├── Include
├── Lib
├── pyvenv.cfg
└── Scripts [87 entries exceeds filelimit, not opening dir]
3)pip 安装你的项目到可编辑状态
myproject
使用安装顶级包pip
。诀窍是-e
在安装时使用标志。这样它就会以可编辑状态安装,并且对 .py 文件所做的所有编辑都将自动包含在已安装的包中。使用 pyproject.toml 和 -e 标志需要 pip >= 21.3
在根目录中运行
pip install -e .
(注意点,它代表“当前目录”)
您还可以使用以下命令安装它pip freeze
Obtaining file:///home/user/projects/myproject
Installing build dependencies ... done
Checking if build backend supports build_editable ... done
Getting requirements to build editable ... done
Preparing editable metadata (pyproject.toml) ... done
Building wheels for collected packages: myproj
Building editable for myproj (pyproject.toml) ... done
Created wheel for myproj: filename=myproj-0.1.0-py2.py3-none-any.whl size=903 sha256=f19858b080d4e770c2a172b9a73afcad5f33f4c43c86e8eb9bdacbe50a627064
Stored in directory: /tmp/pip-ephem-wheel-cache-qohzx1u0/wheels/55/5f/e4/507fdeb40cdef333e3e0a8c50c740a430b8ce84cbe17ae5875
Successfully built myproject
Installing collected packages: myproject
Successfully installed myproject-0.1.0
(venv) PS C: mp est_imports> pip freeze
myproject==0.1.0
4)添加myproject.
到你的导入中
请注意,您只需要添加myproject.
到原本无法工作的导入中。没有pyproject.toml
& 的导入pip install
仍可正常工作。请参阅下面的示例。
测试解决方案
api.py
现在,让我们使用上面定义和下面定义来测试解决方案test_one.py
。
测试一
from myproject.api.api import function_from_api
def test_function():
print(function_from_api())
if __name__ == '__main__':
test_function()
运行测试
(venv) PS C: mp est_imports> python .myproject ests est_one.py
I am the return value from api.api!
这里使用 flit 作为构建后端。还有其他替代方案。
** 实际上,您可以将虚拟环境放在硬盘上的任何位置。
解决方案 2:
七年后
自从我在下面写下答案以来,修改sys.path
仍然是一个快速而肮脏的技巧,适用于私人脚本,但已经有一些改进
安装包(在虚拟环境中或不在虚拟环境中)将为您提供所需的内容,但我建议使用 pip 来执行此操作,而不是直接使用 setuptools(并使用
setup.cfg
来存储元数据)使用
-m
标志并作为包运行也是可行的(但如果您想将工作目录转换为可安装包,就会有点尴尬)。具体来说,对于测试来说,pytest能够在这种情况下找到 api 包并
sys.path
为你处理这些黑客问题
所以这真的取决于你想做什么。不过,就你的情况而言,由于你的目标是在某个时候制作一个合适的包,所以通过安装pip -e
可能是你最好的选择,即使它还不完美。
旧答案
正如其他地方所述,可怕的事实是,您必须进行丑陋的黑客攻击才能允许从__main__
模块的兄弟模块或父包导入。 该问题在PEP 366中有详细说明。PEP 3122试图以更合理的方式处理导入,但 Guido 拒绝了它
唯一的用例似乎是运行恰好位于模块目录中的脚本,我一直将其视为反模式。
(这里)
不过,我经常使用这种模式
# Ugly hack to allow absolute import from the root folder
# whatever its name is. Please forgive the heresy.
if __name__ == "__main__" and __package__ is None:
from sys import path
from os.path import dirname as dir
path.append(dir(path[0]))
__package__ = "examples"
import api
这path[0]
是您正在运行的脚本的父文件夹和dir(path[0])
顶层文件夹。
不过,我仍然无法使用相对导入,但它确实允许从顶层(在示例api
的父文件夹中)进行绝对导入。
解决方案 3:
下面是我在文件夹中的 Python 文件顶部插入的另一种替代方法tests
:
# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
解决方案 4:
sys.path
除非必要,否则你不需要也不应该进行黑客攻击,而在这种情况下,情况并非如此。使用:
import api.api_key # in tests, examples
从项目目录运行:python -m tests.test_one
。
您可能应该移动tests
(如果它们是 api 的单元测试)到内部api
并运行python -m api.test
以运行所有测试(假设有__main__.py
)或者python -m api.test.test_one
运行test_one
。
您还可以__init__.py
从中删除examples
(它不是一个 Python 包)并在安装了的虚拟环境中运行示例api
,例如,如果您有适当的,pip install -e .
虚拟环境中将安装就地包。api
`setup.py`
解决方案 5:
对于 2023 年的读者:如果您对以下内容没有信心pip install -e
:
TL;DR:脚本(通常是入口点)只能执行import
与其级别相同或低于其级别的任何操作。
考虑这种层次结构,正如Python 3 中的相对导入的答案所建议的那样:
MyProject
├── src
│ ├── bot
│ │ ├── __init__.py
│ │ ├── main.py
│ │ └── sib1.py
│ └── mod
│ ├── __init__.py
│ └── module1.py
└── main.py
为了使用简单命令从起点运行我们的程序python main.py
,我们在这里使用绝对导入(没有前导点)main.py
:
from src.bot import main
if __name__ == '__main__':
main.magic_tricks()
的内容bot/main.py
利用显式相对导入来显示我们正在导入的内容,如下所示:
from .sib1 import my_drink # Both are explicit-relative-imports.
from ..mod.module1 import relative_magic
def magic_tricks():
# Using sub-magic
relative_magic(in=["newbie", "pain"], advice="cheer_up")
my_drink()
# Do your work
...
理由如下:
说实话,当我们想要运行我们的 Python 程序时,我们不想给出“好的,所以这是一个模块”的提示。
因此我们使用绝对导入作为入口点
main.py
,这样我们就可以通过简单的方式运行我们的程序python main.py
。在后台,Python 将用来为我们解析包,但是这也意味着,由于try中的路径顺序,
sys.path
我们要导入的包可能会被任何其他同名包取代。sys.path
`import test`
为了避免这些冲突,我们使用显式相对导入。
语法
from ..mod
非常清楚地表明“我们正在导入我们自己的本地包”。但是缺点是,当您想将模块作为脚本运行时,您需要再次考虑“好的,这是一个模块”。
最后,
from ..mod
部分意味着将上升一级MyProject/src
。
结论
将
main.py
脚本放在所有包的根目录旁边MyProject/src
,并使用绝对导入python main.py
来导入任何内容。没有人会创建名为的包src
。那些明确的相对导入将会起作用。
要运行模块,请使用
python -m ...
。
src/
附录:有关以脚本形式运行任何文件的更多信息?
然后你应该使用语法python -m
并看看我的另一篇文章:ModuleNotFoundError:没有名为“sib1”的模块
解决方案 6:
我还不具备足够的 Pythonology 知识,无法理解在不相关项目之间共享代码的预期方式,而无需使用兄弟/相对导入 hack。直到那一天,这是我的解决方案。对于examples
或tests
从导入内容..api
,它看起来像:
import sys.path
import os.path
# Import from sibling directory ..api
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..")
import api.api
import api.api_key
解决方案 7:
对于兄弟包导入,您可以使用sys.path模块的插入或附加方法:
if __name__ == '__main__' and if __package__ is None:
import sys
from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
import api
如果您按如下方式启动脚本,则此方法有效:
python examples/example_one.py
python tests/test_one.py
另一方面,您也可以使用相对导入:
if __name__ == '__main__' and if __package__ is not None:
import ..api.api
在这种情况下,您必须使用“-m”参数启动脚本(请注意,在这种情况下,您不能给出“.py”扩展名):
python -m packageName.examples.example_one
python -m packageName.tests.test_one
当然,你可以混合这两种方法,这样无论如何调用你的脚本都可以工作:
if __name__ == '__main__':
if __package__ is None:
import sys
from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
import api
else:
import ..api.api
解决方案 8:
您需要查看相关代码中的 import 语句是如何编写的。如果examples/example_one.py
使用以下 import 语句:
import api.api
...然后它期望项目的根目录位于系统路径中。
支持此功能的最简单方法(无需任何黑客攻击,正如您所说的)是从顶级目录运行示例,如下所示:
PYTHONPATH=$PYTHONPATH:. python examples/example_one.py
解决方案 9:
以防万一有人在 Eclipse 上使用 Pydev 最终出现在这里:您可以使用Project->Properties并在左侧菜单Pydev-PYTHONPATH下设置外部库,将兄弟的父路径(以及调用模块的父路径)添加为外部库文件夹。然后,您可以从兄弟那里导入,例如。from sibling import some_class
解决方案 10:
我想对np8 提供的解决方案发表评论,但我的声誉不够,所以我只想提一下,您可以完全按照他们的建议创建一个 setup.py 文件,然后pipenv install --dev -e .
从项目根目录执行操作以将其转换为可编辑的依赖项。然后您的绝对导入将起作用,例如from api.api import foo
,您不必处理系统范围的安装。
文档
解决方案 11:
如果您正在使用 pytest,那么pytest 文档描述了如何从单独的测试包引用源包的方法。
建议的项目目录结构是:
setup.py
src/
mypkg/
__init__.py
app.py
view.py
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
文件内容setup.py
:
from setuptools import setup, find_packages
setup(name="PACKAGENAME", packages=find_packages())
以可编辑模式安装软件包:
pip install -e .
pytest 文章引用了Ionel Cristian Mărieș 的这篇博客文章。
解决方案 12:
我制作了一个示例项目来演示如何处理这个问题,这实际上是如上所述的另一个 sys.path 黑客。Python Sibling Import Example,它依赖于:
if __name__ == '__main__': import os import sys sys.path.append(os.getcwd())
只要你的工作目录仍然位于 Python 项目的根目录,这似乎非常有效。