Relative imports for the billionth time

Relative imports for the billionth time

技术背景

在Python中,模块的加载方式和名称对于相对导入有着重要影响。Python文件的加载方式有两种:作为顶层脚本或作为模块。当直接运行Python文件和从其他地方导入该文件时,存在很大区别,文件所在的目录并不能决定Python认为它所属的包,这还取决于加载文件的方式。

实现步骤

1. 理解Script vs. Module

  • 加载方式:直接执行文件(如python myfile.py)时,文件作为顶层脚本加载;在其他文件中遇到import语句时,文件作为模块加载。
  • 命名规则:作为顶层脚本加载时,文件的__name__属性为__main__;作为模块加载时,其名称是文件名,前面加上所属包/子包的名称,用点分隔。

2. 了解相对导入规则

  • 相对导入使用模块的名称来确定其在包层次结构中的位置。
  • 当使用from .. import foo这样的相对导入时,点表示在包层次结构中向上移动一定级别。
  • 若模块名称没有点,则不被视为包的一部分,不能使用from .. import语句。

3. 解决相对导入问题的方法

  • 方法一:使用python -m package.subpackage1.moduleX-m告诉Python将其作为模块加载,而非顶层脚本。
  • 方法二:将调用脚本(如myfile.py)放在package目录之外,并运行它,在脚本中使用标准导入(如from package.moduleA import spam)。

核心代码

示例1:处理相对导入的通用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from __future__ import print_function

if __package__:
print('Package named {!r}; __name__ is {!r}'.format(__package__, __name__))
from .fileA import f1, f2
from .fileB import Class3
else:
print('Not a package; __name__ is {!r}'.format(__name__))
import os, sys
_i = os.path.dirname(os.path.abspath(__file__))
if _i not in sys.path:
print('inserting {!r} into sys.path'.format(_i))
sys.path.insert(0, _i)
else:
print('{!r} is already in sys.path'.format(_i))
del _i

from fileA import f1, f2
from fileB import Class3

if __name__ == '__main__':
import doctest, sys
ret = doctest.testmod()
sys.exit(0 if ret.failed == 0 else 1)

示例2:使用importmonkey进行导入

1
2
3
4
# In test.py
from importmonkey import add_path
add_path("../src/project") # relative to current __file__
import mymodule

示例3:动态编辑__package__sys.path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os
import sys

if not __package__:
__package__ = ((lambda p, d:
(".".join(p[-(n := p[::-1].index(d) + 1):]),
sys.path.insert(0, os.sep.join(p[:-n])))[0])(
os.path.realpath(__file__).split(os.sep)[:-1], "subdir"))

from .script1 import *
from ..script2 import *
from ...script3 import *
from subdir.script3 import *
from script4 import *
from other_folder.script5 import *

最佳实践

  • 明确包的定义:确保正确识别包,避免将非包目录误当作包。
  • 合理使用-m选项:当需要将目录作为包处理时,使用python -m来运行脚本。
  • 避免sys.path滥用:尽量不使用sys.path.append在脚本顶部添加路径,以免影响脚本的可重用性。

常见问题

1. ImportError: attempted relative import with no known parent package

  • 原因:脚本不是包,不允许相对导入。例如,在package/目录下运行python moduleA.pypackage/被添加到sys.path,但脚本不是包。
  • 解决方法:使用python -m package.moduleA,将整个package/视为包。

2. ImportError: attempted relative import beyond top-level package

  • 原因:尝试在包的顶层之上进行相对导入。例如,在package/目录下运行python moduleA.pypackage/不被视为包,而subpackage2被识别为包,此时在subpackage2中使用..进行相对导入会出错。
  • 解决方法:修改相对导入为标准导入,如将from ..subpackage1 import moduleX改为from subpackage1 import moduleX,并使用PYTHONPATH='.' python subpackage2/moduleZ.pypython -m subpackage2.moduleZ运行。

Relative imports for the billionth time
https://119291.xyz/posts/relative-imports-for-the-billionth-time/
作者
ww
发布于
2025年5月26日
许可协议