本文的主要参考文献为python的官方文档,针对的python版本为3.14.2

首先一个语言的导入系统是很重要的,因为它决定了代码的模块化以及可复用性,一个好的模块系统可以让开发者更方便地组织和管理代码,提高代码的可维护性和可读性。而我之所以写这么一篇文章,就是因为一直被python的导入系统折磨,因此才下定决定来搞清楚python的模块系统与导入机制。

对于python来说,我们最常用的导入方式是使用import语句,但是其实除了import语句之外,还有很多其他的导入方式,比如importlib.import_module()以及内置的__import__()函数等。

import语句主要做了两件事情:

  1. 搜索指定名称的模块(就是调用__import__())。
  2. 将搜索的结果绑定到一个local的名称上面。

Example

import spam会产生如下的字节码:

1
spam = import('spam', globals(), locals(), [], 0)

import spam.ham会产生如下的字节码:

1
spam = import('spam.ham', globals(), locals(), [], 0)

from spam.ham import eggs, sausage as saus会产生如下的字节码:

1
2
3
_temp = import('spam.ham', globals(), locals(), ['eggs', 'sausage'], 0)
eggs = _temp.eggs
saus = _temp.sausage

而有关importlib的介绍,暂时还不在本文的考虑范围,本文主要针对于import语句。

Packages介绍

python中有模块(module)和包(package)的概念,模块是一个包含python代码的.py文件,而包则是一个包含多个模块的目录。包可以包含子包和模块,从而形成一个层次化的命名空间。除此之外,最早python的包是需要在目录下放置一个__init__.py文件来标识该目录是一个包,但是从python3.3版本开始,这个文件已经不是必须的了(使用__init__.py的是Regular packages,而不使用__init__.py的是Namespace packages,前者需要手动在__init__.py中来维护__path__变量,而后者则是导入机制自动维护)。而且包和模块被导入之后,会定义许多"dunder"(double underscore)的变量,比如__name____file____path____package__等,这些变量在模块和包的导入过程中起着重要的作用。

__path__为例,该变量就是包(package)被导入时才会定义的变量,其内容为一个列表,包含了该包内部各个模块以及子包(subpackage)的搜索路径,而且可以在包的__init__.py文件中进行修改,从而影响包内部模块的导入行为。

Warning

注意,__path__变量只在包(package)中定义,而在普通的模块(module)中是没有该变量的。

也就是说__path__要么在__init__.py文件中使用,要么import一个包之后,将其作为包的属性来使用。

__path__的示例

假设有如下的包结构:

1
2
3
4
my_package/
|-- init.py
|-- module1.py
|-- module2.py

然后执行如下的python脚本:

1
2
3
import my_package

print(my_package.__path__)

其输出结果会类似于:

['/path/to/directory/containing/my_package']

而依据该变量的结果,当你导入module1module2时,python解释器就知道去哪里寻找这些模块。

1
2
from my_package import module1
from my_package import module2

修改__path__

之所以修改__path__,是因为这样可以将其他的目录变成包my_package的一部分,从而实现更灵活的模块组织和导入。

继续使用上面的包结构,我们可以在__init__.py文件中修改__path__变量:

1
2
3
4
5
# my_package/init.py
import os

# 添加一个新的路径到__path__
path.append(os.path.abspath('../extra_modules'))

这样做了之后,如果extra_modules目录下有一些模块,比如module3.py,我们就可以通过my_package来导入它们:

1
from my_package import module3

导入方式介绍

在使用python的导入功能的时候,我们通常使用的方式是使用import语句,其有两种用法:

1
2
3
4
5
# method 1
import <module_name/package_name> [as <alias_name>]

# method 2
from <package_name/module_name> import <module_name/class_name/function_name> [as <alias_name>]

Note

需要注意的是,如果import package_name后面接的是一个包名,那么实际上导入的是该包的__init__.py文件所定义的内容,如果没有在__init__.py文件中进行特殊处理,package_name目录下的子模块和子包是不会被自动导入的,也无法通过package_name.submodule的方式访问。

而python的导入机制主要分为两种:绝对导入和相对导入。

绝对导入

其中绝对导入可以使用上述的两种import用法,具体的使用语法如下:

1
2
import package_name.module_name [as alias_name]
from package_name.module_name import class_name/function_name [as alias_name]

绝对导入的特点在于,其包的搜索路径都是依据sys.path变量的,而默认情况下,sys.path变量包含了当前脚本所在的目录、PYTHONPATH环境变量中指定的目录以及Python安装目录下的标准库目录等。

假设有如下的项目结构:

1
2
3
4
5
6
/project/
|-- main.py # import my_package.module1
|-- my_package/
|-- __init__.py
|-- module1.py # import module2
|-- module2.py # print("Hello from module2")

当我们在/project目录下运行python my_package/module1.py时,sys.path中会包含/project/my_package目录,因此可以正常运行。但是当我们在/project目录下运行python main.py时,sys.path中会包含/project目录,而不包含/project/my_package目录,因此module1.py中的import module2语句会导致导入失败,因为无法查找到module2模块。

可行的的解决办法如下:最推荐的方法是4

  1. /project/my_package添加到PYTHONPATH环境变量中
  2. module1.py中,将/project/my_package添加到sys.path中,然后才import module2
  3. module1.py中使用绝对导入的方式来导入module2模块,例如from my_package import module2。但是这个会导致python my_package/module1.py无法运行。
  4. module1.py中使用相对导入的方式来导入module2模块,例如from . import module2。这个同样会导致python my_package/module1.py无法运行。但是可以使用python -m my_package.module1来运行。

相对导入

而相对导入只能使用第二种from ... import ...的用法,具体的使用语法如下:其与绝对导入的区别在于,使用相对导入时,模块名称前面会有一个或多个点号(.),单个.表示当前包,两个点号..表示上级包,三个点号...表示上上级包,依此类推。

1
2
3
from .module_name import class_name/function_name [as alias_name]      # 导入当前包中的模块
from ..package_name.module_name import class_name/function_name [as alias_name] # 导入上级包中的模块
from ...package_name.module_name import class_name/function_name [as alias_name] # 导入上上级包中的模块

而相对导入的另一个问题在于,既然是相对导入,那么相对于谁呢?答案是相对于当前模块所在的包(package)。因此,相对导入只能在包内部使用,不能在顶层模块中使用。

Note

其实相对导入其实是相对于该语句所在的模块(或者说文件)的__package__属性来进行导入的。

The module’s __package__ attribute should be set. Its value must be a string, but it can be the same value as its __name__. If the attribute is set to None or is missing, the import system will fill it in with a more appropriate value. When the module is a package, its __package__ value should be set to its __name__. When the module is not a package, __package__ should be set to the empty string for top-level modules, or for submodules, to the parent package’s name. See PEP 366 for further details.
简单来说,__package__属性其实就是模块或者包所处的包的名称,如果是顶层模块,则该属性值为'',如果是包,则该属性值为自己的名称,如果是包中的模块,则该属性值为其父包的名称。

而依据上述关于__package__的描述,我们就知道了为什么相对导入只能在包内部使用,而无法在顶层模块中使用,因为顶层模块的__package__属性值为'',相对导入就不知相对于谁了。

还有一个问题在于对于一个写好的包,其内部其实往往都是使用相对导入的,因为这样可以避免包被移动到其他位置时,导入路径出错的问题。但是有时候我们可能需要直接运行该包内部的某个模块(python my_package/module1.py),这时候就会出现相对导入失败的问题。

出现这个问题的原因在于,使用python my_package/module1.py运行模块时,Python并没有模块的概念,其__package__属性值为None__name__属性值为"__main__",这时候相对导入没有参考点,因此无法正常工作。

而如果通过import my_package.module1来导入该模块时,其__package__属性值为"my_package"__name__属性值为"my_package.module1",只有像这样,__package__有非空的值,相对导入才能正常工作。

这就出现一个很恼人的现象,一个模块可以被main.py导入并正常工作,但是直接运行该模块(比如需要进行测试之类的),却出现ImportError的问题。

而如果通过新建一个main.py,然后通过import来运行(测试)模块的话,不仅很麻烦,而且由于通过import导入的模块,其__name__属性值为"my_package.module1",而不是"__main__",这会导致一些依赖于__name__属性值的代码无法正常工作。

因此,为了解决运行模块的问题,我们可以采用-m选项来运行模块,通过命令行使用python -m my_package.module1来运行模块,这样Python会将模块作为包的一部分来处理,从而正确设置__package__属性,同时其__name__属性值为"__main__",从而依赖于__name__属性值的代码也能正常工作。

Note

需要注意的是,python my_package/module1.pypython -m my_package.module1这两种方式运行模块时,模块的搜索路径(sys.path)是不同的。

  • 使用python my_package/module1.py运行模块时,Python会将module1.py所在的目录添加到sys.path的开头。
  • 使用python -m my_package.module1运行模块时,Python会将该命令运行的目录添加到sys.path的开头。

Note

需要注意的是,python -m main这种情况,Python是有模块的概念的,main.py会被当做顶层模块,其__package__属性值为'',而不是None。但是这种情况仍然是不能使用相对导入的。

实战建议

基于我对于python包导入机制的理解,我总结了如下几点开发建议,以及在实际开发中,推荐使用的目录结构以及导入方法:

  • 在同一个包内部相互导入的时候,统一使用相对导入的方法(package与subpackage之间也是使用相对导入)
  • 在不同包之间导入的时候,统一使用绝对导入的方法
  • main.py等顶层脚本中,使用绝对导入的方法来导入包和模块
  • 要单独运行一个package内部的模块时,使用python -m package.module的方式来运行

而推荐的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/project/
|-- main.py # 使用绝对导入,例如:from my_package1 import module1
|
|-- my_package1/
| |-- __init__.py
| |-- module1.py # 使用相对导入,例如:from . import module2
| |-- module2.py # print("Hello from module2")
| |-- sub_package/
| |-- __init__.py
| |-- sub_module1.py # 使用相对导入,例如:from .. import module1
|
|-- my_package2/
| |-- __init__.py
| |-- moduleA.py # 使用绝对导入,例如:from my_package1 import module1
|
|-- tests/
|-- test_module1.py # 使用绝对导入,例如:from my_package1 import module1

然后不同的模块的运行方式如下:(下面所有的命令都是在/project/目录下运行的)

  • 运行顶层脚本main.pypython main.py 或者 python -m main
  • 运行my_package1包内部的模块module1.pypython -m my_package1.module1
  • 运行my_package1包内部的子包sub_package中的模块sub_module1.pypython -m my_package1.sub_package.sub_module1
  • 运行my_package2包内部的模块moduleA.pypython -m my_package2.moduleA
  • 运行测试模块test_module1.pypython -m tests.test_module1