Python 中包与模块的 import 机制详解
本文深入解析 Python 中 import 语句的工作机制,涵盖包与模块的创建、命名空间隔离原理及多种导入方式。重点讲解直接导入包时子模块不自动加载的原因,以及通过 __all__ 控制 'from...import *' 行为的最佳实践,帮助开发者避免 ModuleNotFoundError 并优化代码结构。
Python import 机制实例解析
我们通过具体实例来解析 import 的实际作用。
首先构建如下包目录结构:创建一个名为 cookFish/ 的文件夹,其中包含两个文件, __init__.py和cookBook.py。
这样命名的原因是:假设我们要用 Python 处理与鱼相关的复杂烹饪任务,因此创建一个名为 cookFish 的包。作为包,其目录下必须包含 __init__.py 文件。此外,因为烧鱼需要菜谱,我们创建了 cookBook.py 文件。对于本次演示,这两个文件已足够,无需创建其他内容。
cookFish/
__init__.py
cookBook.py
在 cookFish/__init__.py 文件中编写如下代码:
__version__ = '0.1'
__author__ = 'XIE Byron'
def cookFish_hello():
print("cookFish_Hello() from cookFish/__init__.py")
在 cookFish/cookBook.py 文件中编写如下代码:
def cookBook_hello():
print("cookBook_hello() from cookBook.py")
注意:以下示例均在 Python 自带的命令行解释器(Windows 环境,Python 3.7)中执行。若使用 Jupyter Notebook 等其他环境,输出结果可能有所不同。
import 包的工作流程与命名空间
执行 'import package-name' 时究竟发生了什么?
以导入包 cookFish 为例。
>>> import cookFish
注意:
若导入时出现 ModuleNotFoundError 错误,示例如下:
>>> import cookFish
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'cookFish'
建议将 Python 的当前工作目录设置为 cookFish 所在父文件夹(即包含 cookFish 文件夹的上级目录)。具体命令如下:
>>> import os
>>> os.chdir(r'path\to\parent\folder\of\cookFish')
使用 dir 命令查看当前命名空间及 cookFish 命名空间下的内容。
>>> dir() # 查看当前命名空间下的对象。注意:此时 cookFish 已存在于当前命名空间中。
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'cookFish', 'os']
>>> dir(cookFish) # 查看 cookFish 命名空间下的对象。
['__author__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', 'cookFish_hello']
其中的 __author__, __version__, cookFish_hello 是我们定义的,它们都被导入到了 cookFish 的命名空间下。然而,cookFish 下的模块 cookBook.py 并未被导入。这是因为直接执行 import cookFish 仅运行 cookFish 文件夹下的 __init__.py 文件,而不会自动运行其他模块,因此 cookBook 未被导入。
注意:Python 中后缀为 .py 的文件称为模块(或脚本);包含 __init__.py 文件的文件夹称为包,通常还包含其他模块。
包与模块的命名空间隔离原理
包与模块的命名空间机制
这里阐述我对“在 cookFish 的命名空间下”这一概念的理解。
Python 执行 import A 时,会运行 A 对应的 Python 代码,并将运行结果存储在名为 A 的命名空间中。
注意:若 A 是包,其代码对应文件夹 A 下的 __init__.py 内容;若 A 是模块,则对应文件 A.py 的内容。
import B 会运行 B 的 Python 代码,并将结果存入名为 B 的命名空间。假设 A 和 B 中均有名为 X 的对象,则 A 中的 X 在当前命名空间中记为 A.X,B 中的 X 记为 B.X,两者在当前命名空间中不会重名。
注意:这里的“对象”指 Python 中的变量/属性、函数、类、实例等。
例如 __version__ 属性(或变量)位于 cookFish 的命名空间下,必须通过 cookFish.__version__ 才能访问。直接输入 __version__ 无法访问,会报错。
直接输入 __version__ 运行将报如下错误:
>>> __version__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name '__version__' is not defined
多种导入方式与别名使用
其他导入包/模块的方式
若想导入 cookFish 下的模块 cookBook,可使用以下语法:
>>> import cookFish.cookBook
此时,cookFish 的命名空间中新增了 cookBook。
>>> dir(cookFish)
['__author__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', 'cookBook', 'cookFish_hello']
随后即可通过全名 cookFish.cookBook 访问 cookBook.py 中的对象,例如:
>>> cookFish.cookBook.cookBook_hello()
cookBook_hello() from cookBook.py
名称过长,能否简化?当然可以:
>>> import cookFish.cookBook as cb
此时当前命名空间中新增对象 cb:
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'cb', 'cookFish', 'os']
随后即可通过别名 cb 访问 cookBook.py 中的对象,例如:
>>> cb.cookBook_hello()
cookBook_hello() from cookBook.py
能否仅将cookBook_hello()导入当前命名空间?当然可以。
>>> from cookFish.cookBook import cookBook_hello
此时 cookBook_hello 被导入到当前命名空间:
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'cb', 'cookBook_hello', 'cookFish', 'os']
随后即可直接访问 cookBook_hello(),无需任何前缀:
>>> cookBook_hello()
cookBook_hello() from cookBook.py
from...import * 与 __all__ 的控制机制
‘from 包/模块名 import *’ 是否导入所有对象?
能否一次性导入 cookFish 下的所有模块和包?答案是可以,也可以不是。
Python 有一条相关指令:
from 包/模块名 import *
例如 from cookFish import *,直观感觉是遍历 cookFish 下所有文件,找到所有子包和模块并全部导入当前命名空间。
然而,该操作在 Windows 和 Mac 系统上无法完美实现。因为这些系统的文件系统无法提供准确的文件名大小写信息。在这两个平台上,Python 无法确定应将 ECHO.py 导入为模块 echo、Echo 还是 ECHO 等(例如 Windows 95 上所有文件名首字母均显示为大写)。若 Python 将 ECHO.py 导入为模块 Echo,但实际代码中按 echo 使用,则会报错。[1]
Python 支持大小写敏感,Echo 和 ECHO 是两个不同的对象。
Python 的唯一解决方案是包作者提供明确的包索引,告知 Python 在代码中如何命名该模块。import 语句遵循以下约定:若在包的 __init__.py 中定义了 __all__ 列表,执行 from xxx import * 时,Python 仅导入 __all__ 列表中的对象。
注意:
__all__ 仅影响 from xxx import *,对其他 import 操作无影响。
在 cookFish/__init__.py 中,我们将函数 cookFish_hello 加入 __all__,代码如下:
__all__ = ['cookFish_hello', ] # added to support `from xxx import *`
__version__ = '0.1'
__author__ = 'XIE Byron'
def cookFish_hello():
print("cookFish_Hello() from cookFish/__init__.py")
重启 Python 解释器,在导入前运行 dir() 显示当前命名空间对象。
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'os']
注意:
为提高运行效率,Python 解释器对同一模块仅导入一次。模块被导入后,再次执行导入命令不会重新加载。为展示 from xx import * 的特殊性,需重启 Python 解释器(即关闭后重新进入)。
随后执行如下操作:
>>> from cookFish import *
输入 dir() 查看 cookFish_Hello() 是否被导入当前命名空间。
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'cookFish_hello', 'os']
可见仅有 __all__ 列表中的 cookFish_hello 被导入当前命名空间,其他内容均未导入,甚至 cookFish 本身也未导入。
因此,“能否一次性导入 cookFish 下的所有模块、所有包?”的答案是:能否一次导入,取决于包作者是否将所有子模块/子包加入 __all__ 列表。
参考
[1] Built-in Package Support in Python 1.5
版本
[1] version 1.0, released on 2019-04-21
[2] version 1.1, released on 2019-04-21
添加了 Python 命令的输出。运行工具为 Windows 版本 Python(3.7) 自带的命令行解释器。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









