我已经尝试阅读有关兄弟进口甚至package documentation的问题,但我还没有找到答案。
具有以下结构:
├── 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中完成,对吧?
由于我在下面写了答案,修改sys.path
仍然是一个快速而肮脏的技巧,适用于私有脚本,但有几个改进
setup.cfg
来存储元数据)-m
flag和作为一个包运行也有效(但如果你想将你的工作目录转换为可安装的包,会变得有点尴尬)。sys.path
hacks所以这真的取决于你想做什么。但是,在你的情况下,因为看起来你的目标是在某个时候制作一个合适的包装,通过pip -e
安装可能是你最好的选择,即使它还不完美。
正如其他地方已经说过的那样,可怕的事实是你必须做丑陋的黑客以允许从__main__
模块从兄弟模块或父包中导入。这个问题在PEP 366中有详细说明。 PEP 3122试图以更合理的方式处理进口,但Guido拒绝了它的说法
唯一的用例似乎是运行脚本,这些脚本恰好位于模块的目录中,我一直将其视为反模式。
(Qazxswpoi)
虽然,我经常使用这种模式
here
这里# 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])
的父文件夹)。
我做了一个示例项目来演示我是如何处理它的,这确实是另一个如上所述的sys.path hack。 import api.api
,依赖于:
PYTHONPATH=$PYTHONPATH:. python examples/example_one.py
只要您的工作目录仍然是Python项目的根目录,这似乎非常有效。如果有人在真实的生产环境中部署它,那么听听它是否在那里工作也会很棒。
首先,您应该避免使用与模块本身同名的文件。它可能打破其他进口。
导入文件时,首先解释器检查当前目录,然后搜索全局目录。
在from sibling import some_class
或Python Sibling Import Example内你可以打电话:
if __name__ == '__main__':
import os
import sys
sys.path.append(os.getcwd())
有很多api
-hacks可用,但我找到了解决手头问题的另一种方法:sys.path.append
。我不确定是否存在与此无关的边缘情况。以下是使用Python 3.6.5(Anaconda,conda 4.5.1),Windows 10计算机进行测试的。
起点是您提供的文件结构,包含在名为setuptools的文件夹中。
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
称为根文件夹,在我的示例中,它位于.
。
作为测试用例,让我们使用以下./api/api.py
C:\tmp\test_imports\
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()
使用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
如果您熟悉虚拟环境,请激活一个,然后跳到下一步。虚拟环境的使用并非绝对必要,但从长远来看,它们将真正帮助您(当您有超过1个项目正在进行时......)。最基本的步骤是(在根文件夹中运行)
from setuptools import setup, find_packages
setup(name='myproject', version='1.0', packages=find_packages())
python -m venv venv
(Linux,macOS)或source ./venv/bin/activate
(Win)要了解更多相关信息,只需谷歌“python虚拟环境教程”或类似内容。除了创建,激活和停用之外,您可能永远不需要任何其他命令。
制作并激活虚拟环境后,控制台应在括号中指定虚拟环境的名称
./venv/Scripts/activate
你的文件夹树应该是这样的**
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]
安装顶级软件包myproject
。诀窍是在安装时使用pip
标志。这样,它以可编辑状态安装,对.py文件所做的所有编辑将自动包含在已安装的包中。
在根目录中,运行
-e
(注意点,它代表“当前目录”)
您还可以看到它是使用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
from myproject.api.api import function_from_api
def test_function():
print(function_from_api())
if __name__ == '__main__':
test_function()
*有关详细的setup.py示例,请参阅(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!
。
**实际上,您可以将虚拟环境放在硬盘上的任何位置。
这是我在setuptools docs文件夹中插入Python文件顶部的另一种方法:
tests
除非有必要,否则你不需要也不应该破解# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
,在这种情况下它不是。使用:
sys.path
从项目目录运行:import api.api_key # in tests, examples
。
你可能应该在qazxsw poi中移动qazxsw poi(如果它们是apis单元测试)并运行python -m tests.test_one
来运行所有测试(假设有tests
)或api
来运行python -m api.test
。
您也可以从__main__.py
中移除python -m api.test.test_one
(它不是Python包)并在安装了test_one
的virtualenv中运行示例,例如,如果你有适当的__init__.py
,在virtualenv中的examples
将安装inplace api
包。
我还没有必要理解Pythonology,以便在没有兄弟/相对导入黑客的情况下看到在不相关项目之间共享代码的预期方式。直到那天,这是我的解决方案。对于pip install -e .
或api
从setup.py
导入东西,它看起来像:
examples
对于兄弟包导入,您可以使用[sys.path] [2]模块的insert或append方法:
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
在这种情况下,您必须使用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
启动脚本(请注意,在这种情况下,您不得提供'.py'扩展名):
python examples/example_one.py
python tests/test_one.py
当然,您可以混合使用这两种方法,这样无论调用方式如何,您的脚本都可以正常工作:
if __name__ == '__main__' and if __package__ is not None:
import ..api.api
TLDR
此方法不需要setuptools,路径入侵,其他命令行参数或在项目的每个文件中指定包的顶级。
只需在父目录中创建一个脚本,无论您调用的是'-m' argument,都可以从那里运行所有内容。有关进一步说明,请继续阅
说明
这可以在不破坏新路径,额外命令行参数或向每个程序添加代码以识别其兄弟的情况下完成。
我之前提到的这个失败的原因是被调用的程序将他们的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
。发生这种情况时,被调用的脚本接受自己位于包的顶层,并拒绝识别兄弟目录中的脚本。
但是,目录顶层下的所有内容仍然会识别顶级下的ANYTHING ELSE。这意味着,要让兄弟目录中的文件相互识别/使用,您必须做的就是从父目录中的脚本调用它们。
概念证明在具有以下结构的目录中:
__main__
__name__
包含以下代码:
__main__
sib1 / call.py包含:
.
|__Main.py
|
|__Siblings
|
|___sib1
| |
| |__call.py
|
|___sib2
|
|__callsib.py
和sib2 / callsib.py包含:
Main.py
如果你重现这个例子,你会注意到调用import sib1.call as call
def main():
call.Call()
if __name__ == '__main__':
main()
会导致“被调用”被打印,就像在import sib2.callsib as callsib
def Call():
callsib.CallSib()
if __name__ == '__main__':
Call()
中定义的那样,即使def CallSib():
print("Got Called")
if __name__ == '__main__':
CallSib()
是通过Main.py
调用的。但是,如果要直接调用sib2/callsib.py
(在对导入进行适当更改之后),则会抛出异常。尽管它在其父目录中的脚本调用时仍然有效,但如果它认为自己位于包的顶层,它将无法工作。
您需要查看如何在相关代码中编写import语句。如果sib2/callsib.py
使用以下import语句:
sib1/call.py
...然后它期望项目的根目录在系统路径中。
最简单的方法是在没有任何黑客攻击的情况下支持(如你所说)将从顶级目录运行示例,如下所示:
sib1/call.py
以防万一在Eclipse上使用Pydev的人最终在这里:您可以使用Project-> Properties并在左侧菜单Pydev-PYTHONPATH下设置外部库,将兄弟的父路径(以及调用模块的父路径)添加为外部库文件夹。然后你可以从兄弟姐妹那里导入,例如。 G。 examples/example_one.py
。