兄弟包导入
我试着阅读了一些关于兄弟模块导入的问题,甚至还看了包的文档,但还是没找到答案。
下面是我的文件结构:
├── 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中实现这个吧?
17 个回答
这里有一个替代方案,我把它放在tests
文件夹里所有Python文件的顶部:
# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
厌倦了 sys.path 的各种小技巧?
网上有很多关于 sys.path.append
的小技巧,但我找到了一种更简单的方法来解决这个问题。
总结
- 把代码放到一个文件夹里(比如叫
packaged_stuff
) - 创建一个
pyproject.toml
文件来描述你的包(下面有个最简单的pyproject.toml
示例) - 用
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
步骤
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
(Windows)
想了解更多,可以在网上搜索“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
├── pyproject.toml
└── venv
├── Include
├── Lib
├── pyvenv.cfg
└── Scripts [87 entries exceeds filelimit, not opening dir]
3) 用可编辑状态安装你的项目
使用 pip
安装你的顶层包 myproject
。关键是使用 -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:\tmp\test_imports> pip freeze
myproject==0.1.0
4) 在导入中添加 myproject.
注意,你只需要在那些不加 myproject.
就无法工作的导入中添加它。那些在没有 pyproject.toml
和 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!
* 这里使用 flit 作为构建后端。还有其他选择。
** 实际上,你可以把虚拟环境放在硬盘的任何地方。
七年之后
自从我写下下面的内容以来,修改 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
的父文件夹)。