问题:兄弟包进口
我已经尝试阅读有关同级导入甚至 包文档的问题,但是我还没有找到答案。
具有以下结构:
├── 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中完成,对吗?
回答 0
七年后
自从我在下面写下答案以来,修改sys.path
仍然是一种快速技巧,对于私有脚本来说效果很好,但是已经有了一些改进
- 安装该软件包(无论是否在virtualenv中)都将为您提供所需的内容,尽管我建议使用pip进行操作,而不是直接使用setuptools(并
setup.cfg
用于存储元数据) - 使用该
-m
标志并作为软件包运行也可以(但是如果要将工作目录转换为可安装的软件包,将会有些尴尬)。 - 对于测试,特别是pytest能够在这种情况下找到api程序包,并
sys.path
为您解决问题
因此,这实际上取决于您要做什么。但是,就您而言,既然您的目标似乎是在某个时候制作一个合适的程序包,那么通过安装pip -e
可能是您最好的选择,即使它尚不完美。
旧答案
正如其他地方已经说过的那样,可怕的事实是,您必须进行丑陋的修改才能允许从同级模块中导入数据或从该__main__
模块中的父级程序包中进行导入。PEP 366中详细介绍了该问题。PEP 3122试图以更合理的方式处理进口货,但圭多拒绝了它的一项
唯一的用例似乎是正在运行的脚本,它们恰好位于模块的目录中,我一直将其视为反模式。
(这里)
不过,我会定期使用这种模式
# 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
的父文件夹中)进行绝对导入。
回答 1
厌倦了sys.path hacks?
有大量的sys.path.append
-hacks,但是我找到了另一种解决问题的方法。
摘要
- 将代码包装到一个文件夹中(例如
packaged_stuff
) setup.py
在使用setuptools.setup()的地方使用创建脚本。- 使用以下命令以可编辑状态安装软件包
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:\tmp\test_imports\
。
api.py
作为测试用例,让我们使用以下./api/api.py
def function_from_api():
return 'I am the return value from api.api!'
test_one.py
from api.api import function_from_api
def test_function():
print(function_from_api())
if __name__ == '__main__':
test_function()
尝试运行test_one:
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'
还尝试相对进口将无法正常工作:
使用from ..api.api import function_from_api
会导致
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
脚步
- 将setup.py文件创建到根目录
的内容为setup.py
*
from setuptools import setup, find_packages
setup(name='myproject', version='1.0', packages=find_packages())
- 使用虚拟环境
如果您熟悉虚拟环境,请激活一个,然后跳到下一步。虚拟环境的使用不是绝对必需的,但从长远来看(当您正在进行多个项目时),它们确实可以帮助您。最基本的步骤是(在根文件夹中运行)
- 创建虚拟环境
python -m venv venv
- 激活虚拟环境
source ./venv/bin/activate
(Linux,macOS)或./venv/Scripts/activate
(Win)
要了解更多信息,只需在Google上搜索“ python虚拟环境教程”或类似内容即可。除了创建,激活和停用之外,您可能根本不需要任何其他命令。
创建并激活虚拟环境后,控制台应在括号中提供虚拟环境的名称。
PS C:\tmp\test_imports> python -m venv venv
PS C:\tmp\test_imports> .\venv\Scripts\activate
(venv) PS C:\tmp\test_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
├── setup.py
└── venv
├── Include
├── Lib
├── pyvenv.cfg
└── Scripts [87 entries exceeds filelimit, not opening dir]
- pip以可编辑状态安装项目
安装您的顶级包myproject
使用pip
。诀窍是-e
在执行安装时使用标志。这样,它以可编辑状态安装,并且对.py文件所做的所有编辑将自动包含在已安装的软件包中。
在根目录中,运行
pip install -e .
(注意点,它代表“当前目录”)
您还可以看到它是通过使用安装的 pip freeze
(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
- 添加
myproject.
到您的进口中
请注意,您将只需要添加myproject.
导入否则将无法正常工作。不能使用setup.py
&导入的导入pip install
仍然可以正常工作。请参见下面的示例。
测试解决方案
现在,让我们使用api.py
上面test_one.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:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!
*有关更多详细的setup.py示例,请参阅setuptools文档。
**实际上,您可以将虚拟环境放在硬盘上的任何位置。
回答 2
这是我在文件tests
夹中的Python文件顶部插入的另一种选择:
# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
回答 3
sys.path
除非有必要,否则您不需要,也不应hack ,在这种情况下则没有必要。用:
import api.api_key # in tests, examples
从项目目录运行:python -m tests.test_one
。
您可能应该tests
在内部移动(如果它们是api的unittests)api
并运行python -m api.test
以运行所有测试(假设存在__main__.py
)或python -m api.test.test_one
改为运行test_one
。
您也可以__init__.py
从examples
(不是Python软件包)中删除该示例,并在api
安装了virtualenv的示例中运行示例,例如,如果您具有适当的条件,则pip install -e .
在virtualenv中将安装就地api
软件包setup.py
。
回答 4
我还没有对Python的理解,没有看到在没有同级/相对导入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
回答 5
对于同级包导入,可以使用[sys.path] [2]模块的insert或append方法:
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
回答 6
TLDR
此方法不需要setuptools,path hacks,其他命令行参数或在项目的每个文件中指定软件包的顶层。
只需在要调用的父目录中创建一个脚本,__main__
然后从那里运行所有内容即可。有关进一步的说明,请继续阅读。
说明
这可以实现,而无需一起寻找新路径,使用额外的命令行参数或向您的每个程序添加代码以识别其兄弟姐妹。
我相信之前提到的失败的原因是被调用的程序将其__name__
设置为__main__
。发生这种情况时,被调用的脚本会接受自身位于程序包的顶层,并拒绝识别兄弟目录中的脚本。
但是,目录顶层下的所有内容仍然可以识别顶层下的任何内容。这意味着要使同级目录中的文件相互识别/利用,您只需要做的就是从其父目录中的脚本中调用它们。
概念证明 在具有以下结构的目录中:
.
|__Main.py
|
|__Siblings
|
|___sib1
| |
| |__call.py
|
|___sib2
|
|__callsib.py
Main.py
包含以下代码:
import sib1.call as call
def main():
call.Call()
if __name__ == '__main__':
main()
sib1 / call.py包含:
import sib2.callsib as callsib
def Call():
callsib.CallSib()
if __name__ == '__main__':
Call()
sib2 / callsib.py包含:
def CallSib():
print("Got Called")
if __name__ == '__main__':
CallSib()
如果重现此示例,您将注意到, 即使通过Main.py
调用,调用也会导致按定义打印“ Got Called” 。但是,如果要直接调用(在对导入进行适当更改之后),则会引发异常。即使它由其父目录中的脚本调用时可以工作,但是如果它认为自己位于程序包的顶层,则它将无法工作。sib2/callsib.py
sib2/callsib.py
sib1/call.py
sib1/call.py
回答 7
我制作了一个示例项目来演示如何处理此问题,这确实是如上所述的另一个sys.path hack。Python Sibling Import Example,它依赖于:
if __name__ == '__main__':
import os
import sys
sys.path.append(os.getcwd())
只要您的工作目录位于Python项目的根目录下,这似乎就非常有效。如果有人将其部署在实际的生产环境中,那么很高兴听到它是否也可以在此环境中工作。
回答 8
您需要查看如何在相关代码中编写导入语句。如果examples/example_one.py
使用以下导入语句:
import api.api
…然后,它希望项目的根目录位于系统路径中。
最简单的方法来支持此操作(如您所说),就是从顶层目录运行示例,如下所示:
PYTHONPATH=$PYTHONPATH:. python examples/example_one.py
回答 9
以防万一有人在Eclipse上使用Pydev的情况出现在这里:您可以使用Project-> Properties并在左侧菜单Pydev-PYTHONPATH下设置External Libraries,将同级的父路径(以及调用模块的父路径)添加为外部库文件夹。然后,您可以从同级导入,例如。from sibling import some_class
回答 10
首先,应避免使用与模块本身同名的文件。它可能会破坏其他进口。
导入文件时,首先解释器检查当前目录,然后搜索全局目录。
里面examples
或者tests
您可以拨打:
from ..api import api