模块
绝对导入和相对导入为 Python 包的组织提供了清晰的规则。明确的导入方式让代码的依赖关系更加清晰,避免了命名冲突。
# 绝对导入
from mypackage.submodule import function
# 相对导入
from .submodule import function
from ..parentmodule import other_function
模块
模块表示一个或多个具有相关功能的 Python 代码的集合。
最小的模块是单个 Python 可执行文件。最常见的是 .py 后缀结尾的文件。
较大的模块往往是一个文件夹,内含多个 Python 可执行文件、文件夹与资源等等。
Python 的 import 不止能导入 .py 后缀结尾的文件
.pyd 是 Windows 特有的文件格式。它的作用和 .dll 相似,可直接导入而不需要 ctypes 模块。
.pyc 是由 py 文件经过编译后生成的二进制文件,py 文件变成 .pyc 文件后,加载的速度有所提高,并且可以实现源码隐藏。
Python 的 import 通过 importlib 模块自动处理这些不同格式的文件。
.so 和 .dll 分别是 Linux 和 Windows 的动态链接库,可使用 ctypes 模块导入。
.pyx 是 Cython 的源代码文件,支持 Python 与 C 代码混合编程。可以将 .pyx 文件编译为纯 C 文件,但需要注意的是,这个 C 文件通常是用来创建一个 Python 扩展模块的,它不能独立运行。需要进一步编译为 .so 或 .dll 或 .pyd 文件。
.pyi 文件是 Python 接口文件(Python Interface file),也称为存根文件(stub file)。它包含模块的类型提示,但不包含实际的实现代码。当一个模块是用 C 或 Cython 编写的(例如,编译成 .pyd 或 .so 文件),它的源代码不是 Python 格式,因此无法直接被 MyPy、Pyright 等类型检查器或 IDE 识别。这时,开发者会创建一个对应的 .pyi 文件来提供类型信息。
from,import,as
在导入模块时,可以使用from、import、as关键字来导入模块中的指定内容。
通过import关键字导入指定的模块。
通过from关键字从模块中导入指定内容。
通过as关键字给函数别名。
def foo():
print('goodbye, world!')
import module # 导入整个模块文件
module.foo()# 输出goodbye, world!
from module import foo # 从模块中导入foo函数
foo() # 输出goodbye, world!
from module import foo as foo2 # 导入模块中的foo函数,并别名为foo2
foo2() # 输出goodbye, world!
__name__ 属性
__name__ 属性是一个特殊的属性,用于获取模块的名称。
print(__name__)
"""
当模块被直接运行时,__name__的值为__main__,即便在多线程和协程中也是__main__
3.14多进程中,会将子进程的__name__设为__mp_main__
当模块被导入时,__name__的值为模块的名称,即module
如果你希望在模块被直接运行时执行一些代码,被导入时则不执行,可以这样写:
"""
if __name__ == "__main__":
print("This is the main module")
模块的其他特殊属性(魔法变量)
除了 __name__ 外,Python 模块还有一系列由解释器自动设置或约定的特殊属性,常用于内省、文档和加载逻辑。
| 属性 | 说明 |
|---|---|
__doc__ | 模块的文档字符串(即文件开头的多行字符串),若未定义则为 None。 |
__file__ | 加载该模块时使用的文件路径(若从文件加载)。交互式环境、内置模块或动态生成的模块可能没有此属性。 |
__package__ | 模块所属的包名。顶层模块为 '',包内模块为包名(如 mypkg.sub);若无法确定则为 None。 |
__path__ | 仅对包有效,为包内搜索子模块的路径列表(通常为包目录路径)。普通模块无此属性。 |
__loader__ | 加载该模块的 loader 对象,属于 importlib.abc.Loader。 |
__spec__ | 该模块的 ModuleSpec 对象,描述模块名、loader、来源等,由 importlib 使用。 |
__dict__ | 模块的命名空间(全局变量、函数、类等)组成的字典。 |
__annotations__ | 模块级变量注解组成的字典(PEP 526),未注解则为空 dict。 |
__builtins__ | 在模块全局作用域中,指向内置名称(通常为 builtins 模块或其中 dict)。 |
# 在任意 .py 文件中或交互式环境中:
import sys
print(__name__) # 当前模块名或 '__main__'
print(__doc__) # 本模块的 docstring
print(__file__) # 本模块文件路径(若存在)
print(__package__) # 所属包名或 ''
print(__loader__) # 加载本模块的 loader
print(__spec__.name) # 模块名
print("__path__" in dir(sys.modules[__name__])) # 普通模块通常为 False
项目中可用 test/run.py 与 test/test.py 做对照实验:test.py 定义了一个带 docstring 的模块,run.py 里 import test 后分别打印「当前模块 run」和「被导入模块 test」的 __name__、__doc__、__file__、__package__、__loader__、__spec__.name 以及是否包含 __path__。在项目根目录执行 python test/run.py 即可查看输出。
- 内置模块(如
sys、builtins)往往没有__file__,因为不是从磁盘上的.py文件加载的。 - 使用
importlib.util.spec_from_loader等动态创建的模块,其__file__可能为None或由 loader 自定义。 - 包(含
__path__的模块)的__file__通常指向该包的__init__.py所在目录。
__init__.py
通常情况下,当使用 import 语句导入模块后,Python 会按照以下顺序查找指定的模块文件:
当前目录,即当前执行的程序文件所在目录下查找;
到 PYTHONPATH(环境变量)下的每个目录中查找;
到 Python 默认的安装目录下查找。
以上所有涉及到的目录,都保存在标准模块 sys 的 sys.path 变量中,通过此变量我们可以看到指定程序文件支持查找的所有目录。
换句话说,如果要导入的模块没有存储在 sys.path 显示的目录中,那么导入该模块并运行程序时,Python 解释器就会抛出 ModuleNotFoundError(未找到模块)异常。
解决“Python 找不到指定模块”的方法可以是:
- 向 sys.path 中临时添加模块文件存储位置的完整路径;
- 将模块放在 sys.path 变量中已包含的模块加载路径中;
- 设置 PYTHONPATH 系统环境变量。
具体区别可以创建下面的文件结构来了解:
top/
├── __init__.py
├── second.py
└── second_copy.py
在 Python 中,__init__.py 文件是一个特殊的文件,它标志着一个目录是一个 Python 包(package)。当 Python 解释器在导入一个模块时,如果发现一个目录里包含了 init.py 文件,它就会将这个目录当作一个包来处理。
__init__.py 文件本身可以为空,但它也可以包含初始化包的代码。例如,你可以在里面定义变量、函数,或者导入子模块,以便在包被导入时自动执行这些代码。这使得你可以控制包的初始化行为,比如设置包级别的配置或简化子模块的导入路径。
import sys
print(sys.argv)
import sys
print(sys.argv)
from .second_copy import *
两种运行脚本的方式,以及对应的输出:
-
使用
python -m top.second运行:PS C:\Users\jiang\Desktop> python -m top.second
['C:\\Users\\jiang\\Desktop\\top\\second.py']
['C:\\Users\\jiang\\Desktop\\top\\second.py']- 输出两次相同的
sys.argv,显示脚本的完整路径。 - 没有错误,脚本正常运行。
- 输出两次相同的
-
直接运行
python top\second.py:PS C:\Users\jiang\Desktop> python top\second.py
['top\\second.py']
Traceback (most recent call last):
File "C:\Users\jiang\Desktop\top\second.py", line 3, in <module>
from .second_copy import *
ImportError: attempted relative import with no known parent package- 仅输出了
sys.argv一次,显示的是相对路径top\second.py。 - 然后抛出了
ImportError,提示“尝试进行相对导入,但没有已知的父包”。
- 仅输出了
为什么会有这样的差异?
问题的核心在于 Python 如何处理这两种运行方式,以及它们对模块结构和相对导入的影响。
1. python -m top.second 的行为
- 运行方式:使用
-m标志告诉 Python 将top.second作为一个模块运行。这里,top被识别为一个包,second是该包中的一个模块。 - 包上下文:Python 会正确设置包的层次结构。由于当前工作目录是
C:\Users\jiang\Desktop,Python 知道top是一个包,并且second.py是其中的模块。 - 相对导入:在
second.py中,from .second_copy import *是一个相对导入,.表示当前包(即top)。因为 Python 已经建立了包上下文,它能找到同一目录下的second_copy.py,导入成功。 sys.argv的值:在这种模式下,sys.argv[0]被设置为脚本的完整路径,即C:\\Users\\jiang\\Desktop\\top\\second.py。- 首先,
second.py打印这个值。 - 然后,导入
second_copy.py时,second_copy.py也打印sys.argv,因为sys.argv是全局的,不会因模块不同而改变,所以输出两次相同的结果。
- 首先,
2. python top\second.py 的行为
- 运行方式:直接通过文件路径运行
second.py,即将其作为独立的脚本执行,而不是作为一个包中的模块。 - 包上下文缺失:在这种情况下,Python 不会将
top视为一个包,而是直接运行second.py作为主模块(__main__)。因此,没有定义任何“父包”。 - 相对导入失败:
second.py中的from .second_copy import *依赖于包结构,但由于缺少包上下文,Python 不知道.代表什么,导致抛出ImportError: attempted relative import with no known parent package。 sys.argv的值:在这里,sys.argv[0]是命令行中提供的路径,即top\second.py(相对于当前工作目录C:\Users\jiang\Desktop)。second.py打印这个值后,尝试执行相对导入时失败,因此程序终止,second_copy.py的代码未被执行。
__main__.py
接下来我们添加一个__main__.py文件。它相当于是为包提供一个脚本入口。
top/
├── __init__.py
├── __main__.py
├── second.py
└── second_copy.py
def add_one(x):
return x + 1