我试着通读关于兄弟姐妹导入的问题,甚至
软件包文档,但我还没找到答案。
结构如下:
├── 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
示例和测试目录中的脚本如何从
API模块和从命令行运行?
另外,我希望避免对每个文件都使用难看的sys.path.insert。肯定
这可以在Python中完成,对吧?
对于2021年的读者:如果你对pip没有信心,请安装-e:
考虑一下这个层次结构,正如Python 3中相对导入的答案所推荐的那样:
MyProject
├── src
│ ├── bot
│ │ ├── __init__.py
│ │ ├── main.py
│ │ └── sib1.py
│ └── mod
│ ├── __init__.py
│ └── module1.py
└── 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 MyProject/main.py时,/到/MyProject的路径被添加到sys.path中。
绝对导入import src。机器人会读的。
从…mod部分意味着它将上升一级到MyProject/src。
我们能看看吗?是的,因为路径/to/MyProject被添加到sys.path中。
所以重点是:
我们应该把主脚本放在MyProject/src旁边,因为当做相对引用时,我们不会离开src,绝对导入导入src。为我们提供合适的作用域:src/ scope。
参见:ModuleNotFoundError:没有名为“sib1”的模块
对于兄弟包导入,您可以使用[sys. xml]的insert或append方法。路径][2]模块:
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
存在的问题:
你只是不能让import mypackage在test.py中工作。你将需要一个可编辑的安装,改变到路径,或改变__name__和路径
demo
├── dev
│ └── test.py
└── src
└── mypackage
├── __init__.py
└── module_of_mypackage.py
--------------------------------------------------------------
ValueError: attempted relative import beyond top-level package
解决方案:
import sys; sys.path += [sys.path[0][:-3]+"src"]
在尝试在test.py中导入之前,请将上述内容放在上面。这是它。你现在可以导入mypackage了。
这在Windows和Linux上都可以工作。它也不会关心从哪个路径运行脚本。它足够短,可以拍打任何你需要它的地方。
为什么有效:
The sys.path contains the places, in order, where to look for packages when attempting imports if they are not found in installed site packages. When you run test.py the first item in sys.path will be something like /mnt/c/Users/username/Desktop/demo/dev i.e.: where you ran your file. The oneliner will simply add the sibling folder to path and everything works. You will not have to worry about Windows vs Linux file paths since we are only editing the last folder name and nothing else. If you project structure is already set in stone for your repository we can also reasonably just use the magic number 3 to slice away dev and substitute src
TLDR
这种方法不需要setuptools、路径hack、额外的命令行参数,也不需要在项目的每个文件中指定包的顶层。
只要在你调用的__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/ callsibb .py包含:
def CallSib():
print("Got Called")
if __name__ == '__main__':
CallSib()
如果你重现这个例子,你会注意到调用Main.py会导致“Got Called”被打印出来,就像在sib2/ callsibb .py中定义的那样,即使sib2/ callsibb .py是通过sib1/call.py调用的。但是,如果直接调用sib1/call.py(在对导入进行适当更改之后),则会抛出异常。即使它在其父目录中被脚本调用时可以工作,但如果它认为自己位于包的顶层,它就不能工作。