233. Python 模块间互相引用

目录如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
src
├── __init__.py
├── experiments
│   ├── __init__.py
│   └── main.py
├── pyrightconfig.json
├── subdir_1
│   ├── __init__.py
│   ├── file_1_1.py
│   └── subsubdir_1_1
│   ├── __init__.py
│   └── file_1_1_1.py
└── subdir_2
├── __init__.py
├── file_2_1.py
├── file_2_2.py
└── subsubdir_2_1
├── __init__.py
├── file_2_1_1.py
└── file_2_1_2.py

字符串的相对路径

在 python 文件中所有出现的字符串 "./" 路径均为运行 python 命令所在的目录路径

  • 在运行文件的所在文件夹下运行: python ./main.py 中的"./" 路径为 ./
  • 跨文件夹运行: python ./subpath/subsubpath/main.py, 所有 "./" 表示的路径仍然为 "./", 而不是 ./subpath/subsubpath

相对路径导入模块的原则

  1. 需要在 sys.path 中能够找到需要 import 的模块

    sys.path 的路径为默认包括主模块 (这里是 main.py) 父目录路径.

    不管是在 ./ 下运行 python ./main.py 还是在 ../ 路径下运行 python ./experiments/main.py, sys.path 的路径永远包含主模块所在目录的绝对路径.

  2. 使用相对导入时, 使用 ..... 等等指向的目录不能超过 top-level 包, 即与起始 import (在哪个模块开始 import 的就是起始 import) 的第一层的路径同级.

    import 是个迭代模式的.

    假设如下模块导入代码在主模块 (其他模块相同) 中为例:

    • from src.subdir_2 import file_2_1 的 import 的 top-level 就是 src
    • from subdir_1 import file_1_1 的 import 的 top-level 就是 subdir_1
  3. 主模块只能使用绝对路径导入

    如: from subdir_1 import file_1_1, 而不能使用 from ..subdir_1 import file_1_1

模块导入方式

模块路径以混合方式导入 (推荐)

假设以 src 为根目录, 并在根目录下运行 python 命令, 那么模块导入方式如下

  • 凡是涉及到项目根目录的模块使用绝对路径导入

    如这里的: subdir_1, subdir_2 中只有彼此引用到了, 都使用绝对路径导入

  • 没有涉及到项目根目录的模块使用相对路径导入

    如这里的: subdir_1 中的模块之间的导入, 即 subdir_*/ (正则表达式) 中的模块之间的导入

实例

目录见这里, 在 src 目录运行命令: python ./experiments/main.py, 点击这里下载 源码.

[!IMPORTANT]
如果使用的是 neovim 的 pyright, 那么需要指定项目根目录才可.
如果是当前目录 ./, 可以直接创建空文件 ./pyrightconfig.json, 并使用 nvim ./
更多细节 (见 234. neovim pyright 环境配置)
如果还需要包含其他的搜索目录, 那么 pyrightconfig.json 中添加如下内容

具体代码如下所示:

  • __init__.py

  • experiments

    • __init__.py

    • main.py

      1
      2
      3
      4
      import sys
      sys.path.append("./")
      print(sys.path)
      from subdir_1 import file_1_1

      这里有由于 main.py 的路径为 ./experiments/main.py, 所以 sys.path 中只有路径 ./experiments 的绝对路径, 而没有 ./ 的绝对路径.
      从而通过 sys.path.append("./") 来添加 ./ 路径, 其可通过 print(sys.path) 查看

  • subdir_1

    • __init__.py

    • file_1_1.py

      1
      2
      3
      4
      5
      6
      7
      8
      # 同模块调用
      from .subsubdir_1_1 import file_1_1_1

      # 跨模块调用
      from subdir_2 import file_2_1

      def main():
      print(__file__)
    • subsubdir_1_1

      • __init__.py

      • file_1_1_1.py

        1
        2
        3
        4
        from subdir_2.subsubdir_2_1 import file_2_1_2

        def main():
        print(__file__)
  • subdir_2

    • __init__.py

    • file_2_1.py

      1
      2
      3
      4
      5
      from . import file_2_2
      from .subsubdir_2_1 import file_2_1_1, file_2_1_2

      def main():
      print(__file__)
    • file_2_2.py

      1
      2
      def main():
      print(__file__)
    • subsubdir_2_1

      • __init__.py

      • file_2_1_1.py

        1
        2
        def main():
        print(__file__)
      • file_2_1_2.py

        1
        2
        def main():
        print(__file__)

纯相对路径引用

不管怎么样, 只要是 python path/to/your/main.py, 主模块(main.py) 都要使用绝对路径导入模块.
只需要满足 相对路径导入模块核心点 即可.

如果找不到模块, 可通过如下代码添加模块搜索路径:

1
2
import sys
sys.path.append("./")

上述代码将运行路径添加进 sys.path 中, 那么 sys.path 中的 "./" 路径指的是运行命令的目录 (这里是 src), 而不是 python 主文件所在目录, 可通过 print(os.path.abspath("./")) 查看.

实例

目录见这里, 在 src 目录运行命令: python ./experiments/main.py, 点击这里下载 源码.

  • __init__.py

  • experiments

    • main.py

      1
      2
      3
      4
      5
      import os
      import sys
      sys.path.append("..") # add path: os.path.abspath("../")
      print(sys.path)
      from suboptimum_way_src.subdir_1 import file_1_1
  • subdir_1

    • __init__.py

    • file_1_1.py

      1
      2
      3
      4
      5
      6
      7
      8
      # 同模块调用
      from .subsubdir_1_1 import file_1_1_1

      # 跨模块调用
      from ..subdir_2 import file_2_1

      def main():
      print(__file__)
    • subsubdir_1_1

      • __init__.py

      • file_1_1_1.py

        1
        2
        3
        4
        from ...subdir_2.subsubdir_2_1 import file_2_1_2

        def main():
        print(__file__)
  • subdir_2

    • __init__.py

    • file_2_1.py

      1
      2
      3
      4
      5
      from . import file_2_2
      from .subsubdir_2_1 import file_2_1_1, file_2_1_2

      def main():
      print(__file__)
    • file_2_2.py

      1
      2
      def main():
      print(__file__)
    • subsubdir_2_1

      • __init__.py

      • file_2_1_1.py

        1
        2
        def main():
        print(__file__)
      • file_2_1_2.py

        1
        2
        def main():
        print(__file__)

常见错误

attempted relative import beyond top-level package

src 目录文件如下所示:

  • __init__.py

  • main.py

    1
    2
    3
    4
    5
    import os
    import sys
    sys.path.append(os.path.abspath("../"))
    print(sys.path)
    from subdir_1 import file_1_1
  • subdir_1

    • __init__.py

    • file_1_1.py

      1
      2
      3
      4
      5
      from suboptimum_way_src.subdir_1.subsubdir_1_1 import file_1_1_1
      from ..subdir_2 import file_2_1

      def main():
      print(__file__)
    • subsubdir_1_1

      • __init__.py

      • file_1_1_1.py

        1
        2
        3
        4
        from ...subdir_2.subsubdir_2_1 import file_2_1_1

        def main():
        print(__file__)
  • subdir_2

    • __init__.py

    • file_2_1.py

      1
      2
      3
      4
      5
      from . import file_2_2
      from .subsubdir_2_1 import file_2_1_1, file_2_1_2

      def main():
      print(__file__)
    • file_2_2.py

      1
      2
      def main():
      print(__file__)
    • subsubdir_2_1

      • __init__.py

      • file_2_1_1.py

        1
        2
        def main():
        print(__file__)
      • file_2_1_2.py

        1
        2
        def main():
        print(__file__)

由于 main.py 中使用了使用 from subdir_1 import file_1_1 导入模块, 那么 top-level 就是 suboptimum_way_src.

而这时即使 file_1_1.py 文件使用 from suboptimum_way_src.subdir_1.subsubdir_1_1 import file_1_1_1 能够明确出来 suboptimum_way_srcsubdir_1 的关系.
from ...subdir_2.subsubdir_2_1 import file_2_1_1 明确了 suboptimum_way_srcsubdir_2 的关系
但是对于 from ..subdir_2 import file_2_1 中的 subdir_2 是否是 suboptimum_way_src/subdir_2 是一个呢?
也可能是与 suboptimum_way_src 同级的 subdir_2
验证方法很简单, 只需要将 file_1_1.py 修改成如下:

1
2
3
4
5
6
7
8
9
# 同模块调用
from suboptimum_way_src.subdir_1.subsubdir_1_1 import file_1_1_1

# 跨模块调用
from suboptimum_way_src.subdir_2 import file_2_1
from ..subdir_2 import file_2_2

def main():
print(__file__)

仍然是报错:

1
2
3
4
5
6
7
['/Users/qeuroal/abspath_relatpath/suboptimum_way_src', '/Users/qeuroal/anaconda3/envs/torch/lib/python312.zip', '/Users/qeuroal/anaconda3/envs/torch/lib/python3.12', '/Users/qeuroal/anaconda3/envs/torch/lib/python3.12/lib-dynload', '/Users/qeuroal/anaconda3/envs/torch/lib/python3.12/site-packages', '/Users/qeuroal/abspath_relatpath']
Traceback (most recent call last):
File "/Users/qeuroal/abspath_relatpath/suboptimum_way_src/main.py", line 5, in <module>
from subdir_1 import file_1_1
File "/Users/qeuroal/abspath_relatpath/suboptimum_way_src/subdir_1/file_1_1.py", line 6, in <module>
from ..subdir_2 import file_2_2
ImportError: attempted relative import beyond top-level package

修正方法

file_1_1.py 改为如下模块导入方法

1
2
3
4
5
6
7
8
# 同模块调用
from suboptimum_way_src.subdir_1.subsubdir_1_1 import file_1_1_1

# 跨模块调用
from suboptimum_way_src.subdir_2 import file_2_1

def main():
print(__file__)

下面是猜想:

使用相对导入时, 使用 .. 目录不能于主模块中第一层 import 的路径

  • from suboptimum_way_src.subdir_2 import file_2_1 的 import 的 top-level 就是 suboptimum_way_src
  • from subdir_1 import file_1_1 的 import 的 top-level 就是 subdir_1

这是个迭代模式的、树状展开的