Sibling package imports
我试着通读有关兄弟姐妹进口的问题,甚至打包文档,但我还没有找到答案。
具有以下结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 | ├── 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 |
另外,我想避免对每个文件进行难看的
厌倦了系统路径黑客?
有很多可用的
起始点是您提供的文件结构,包装在名为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | . └── 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 |
我将把
作为一个测试用例,让我们使用下面的/api/api.py
1 2 | def function_from_api(): return 'I am the return value from api.api!' |
TestyOn.Py
1 2 3 4 5 6 7 | from api.api import function_from_api def test_function(): print(function_from_api()) if __name__ == '__main__': test_function() |
试着运行测试一:
1 2 3 4 5 | PS C:\tmp\test_imports> python .\myproject\tests\test_one.py Traceback (most recent call last): File".\myproject\tests\test_one.py", line 1, in <module> from api.api import function_from_api ModuleNotFoundError: No module named 'api' |
同样尝试相对进口也不会奏效:
使用
1 2 3 4 5 | PS C:\tmp\test_imports> python .\myproject\tests\test_one.py Traceback (most recent call last): File".\tests\test_one.py", line 1, in <module> from ..api.api import function_from_api ValueError: attempted relative import beyond top-level package |
步骤1)将setup.py文件创建到根级别目录
1 2 3 | from setuptools import setup, find_packages setup(name='myproject', version='1.0', packages=find_packages()) |
2)使用虚拟环境
如果您熟悉虚拟环境,请激活其中一个,然后跳到下一步。虚拟环境的使用并不是绝对必要的,但从长远来看,它们确实会帮助您(当您有一个以上的项目正在进行时)。最基本的步骤是(在根文件夹中运行)
- 创建虚拟环境
python -m venv venv
- 激活虚拟环境
source ./venv/bin/activate (linux,macos)或./venv/Scripts/activate (win)
要了解更多信息,只需谷歌出"python virtual env tutorial"或类似内容。除了创建、激活和停用之外,您可能永远不需要任何其他命令。
创建并激活虚拟环境后,控制台应在括号中给出虚拟环境的名称。
1 2 3 | PS C:\tmp\test_imports> python -m venv venv PS C:\tmp\test_imports> .\venv\Scripts\activate (venv) PS C:\tmp\test_imports> |
您的文件夹树应该像这样**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | . ├── 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 ├── setup.py └── venv ├── Include ├── Lib ├── pyvenv.cfg └── Scripts [87 entries exceeds filelimit, not opening dir] |
3)pip在可编辑状态下安装项目
使用
在根目录中,运行
您还可以看到它是通过使用
1 2 3 4 5 6 7 | (venv) PS C:\tmp\test_imports> pip install -e . Obtaining file:///C:/tmp/test_imports Installing collected packages: myproject Running setup.py develop for myproject Successfully installed myproject (venv) PS C:\tmp\test_imports> pip freeze myproject==1.0 |
4)在进口产品中增加
请注意,您只需将
现在,让我们使用上面定义的
1 2 3 4 5 6 7 | from myproject.api.api import function_from_api def test_function(): print(function_from_api()) if __name__ == '__main__': test_function() |
运行测试
1 2 | (venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py I am the return value from api.api! |
*有关更详细的setup.py示例,请参阅setuptools文档。
**实际上,您可以将虚拟环境放在硬盘上的任何位置。
七年后
由于我在下面写了答案,修改
- 安装软件包(无论是否在virtualenv中)都可以满足您的需要,不过我建议使用pip来完成,而不是直接使用安装工具(以及使用
setup.cfg 来存储元数据)。 - 使用
-m 标志并作为包运行也可以工作(但如果您想将工作目录转换为可安装的包,结果会有点尴尬)。 - 具体来说,对于测试,pytest能够在这种情况下找到API包,并为您处理
sys.path 黑客程序。
所以这取决于你想做什么。不过,在您的情况下,由于您的目标似乎是在某个时候制作一个合适的软件包,所以通过
正如其他地方已经提到的,可怕的事实是,您必须进行丑陋的黑客攻击,以允许从兄弟模块或父包中从
The only use case seems to be running scripts that happen
to be living inside a module's directory, which I've always seen as an
antipattern.
(这里)
不过,我经常使用这种模式
1 2 3 4 5 6 7 8 9 10 | # 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 |
这里,
不过,我仍然不能将相对导入与此一起使用,但它允许从顶层绝对导入(在您的示例中是
下面是我在
1 2 3 | # Path hack. import sys, os sys.path.insert(0, os.path.abspath('..')) |
你不需要也不应该入侵
1 | import api.api_key # in tests, examples |
从项目目录运行:
您可能应该将
您还可以从
我还没有理解在没有兄弟/相关的导入黑客的情况下,如何在不相关的项目之间共享代码的预期方式所必需的pythology。直到那天,这是我的解决办法。对于
1 2 3 4 5 6 | 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 |
对于兄弟包导入,可以使用[sys.path][2]模块的insert或append方法:
1 2 3 4 5 | 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 |
如果您按如下方式启动脚本,这将起作用:
1 2 | python examples/example_one.py python tests/test_one.py |
另一方面,您也可以使用相对导入:
1 2 | if __name__ == '__main__' and if __package__ is not None: import ..api.api |
在这种情况下,您必须使用"-m"参数启动脚本(注意,在这种情况下,您不能提供".py"扩展名):
1 2 | python -m packageName.examples.example_one python -m packageName.tests.test_one |
当然,您可以混合使用这两种方法,这样无论脚本如何调用,它都可以工作:
1 2 3 4 5 6 7 8 | 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 |
以防Eclipse上使用pydev的人在这里结束:您可以使用project->properties将兄弟姐妹的父路径(以及调用模块的父路径)添加为外部库文件夹,并在左侧菜单pydev pythonpath下设置外部库。然后你可以从你的兄弟姐妹那里进口,如
您需要查看导入语句是如何用相关代码编写的。如果
1 | import api.api |
…然后它期望项目的根目录在系统路径中。
最简单的方法是在没有任何黑客的情况下(如您所说)从顶层目录运行示例,如下所示:
1 | PYTHONPATH=$PYTHONPATH:. python examples/example_one.py |
TLDR
此方法不需要安装工具、路径黑客、其他命令行参数,也不需要在项目的每个文件中指定包的顶级。
只需在父目录中编写一个脚本,无论您所调用的是什么,都将成为您的
解释
这可以在不破坏新路径、额外的命令行参数或向每个程序添加代码以识别其兄弟程序的情况下完成。
正如我之前提到的,失败的原因是调用的程序将它们的
但是,目录顶层下的所有内容仍然可以识别顶层下的其他内容。这意味着,要让兄弟目录中的文件相互识别/利用,唯一需要做的就是从父目录中的脚本调用它们。
概念证明在具有以下结构的目录中:
1 2 3 4 5 6 7 8 9 10 11 12 | . |__Main.py | |__Siblings | |___sib1 | | | |__call.py | |___sib2 | |__callsib.py |
1 2 3 4 5 6 7 8 9 | import sib1.call as call def main(): call.Call() if __name__ == '__main__': main() |
sib1/call.py包含:
1 2 3 4 5 6 7 8 9 | import sib2.callsib as callsib def Call(): callsib.CallSib() if __name__ == '__main__': Call() |
sib2/callsib.py包含:
1 2 3 4 5 | def CallSib(): print("Got Called") if __name__ == '__main__': CallSib() |
如果复制此示例,您会注意到调用
首先,您应该避免使用与模块本身同名的文件。它可能会破坏其他进口产品。
导入文件时,首先解释器检查当前目录,然后搜索全局目录。
在
1 | from ..api import api |