兄弟包导入

381 投票
17 回答
231607 浏览
提问于 2025-04-16 19:25

我试着阅读了一些关于兄弟模块导入的问题,甚至还看了包的文档,但还是没找到答案。

下面是我的文件结构:

├── 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

那么,examplestests文件夹里的脚本怎么才能从api模块导入,并且能在命令行中运行呢?

另外,我想避免在每个文件里都用那种丑陋的sys.path.insert方法。肯定有更好的办法在Python中实现这个吧?

17 个回答

51

这里有一个替代方案,我把它放在tests文件夹里所有Python文件的顶部:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
424

厌倦了 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.tomlpip 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 作为构建后端。还有其他选择。

** 实际上,你可以把虚拟环境放在硬盘的任何地方。

119

七年之后

自从我写下下面的内容以来,修改 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 的父文件夹)。

撰写回答