# 模块
# import
一个 py 脚本文件,可以被 Python 解释器直接运行,也可以通过 import 语句导入其它脚本文件,此时该脚本文件称为一个模块(module)。
用关键字
import
可以将某个模块,导入到当前脚本。- 例:
>>> import sys # 导入一个名为 sys 的模块 >>> sys <module 'sys' (built-in)> >>> sys.platform # 调用 sys 模块的成员 'linux'
- 例:
执行
from xx import xx
语句,可以从某个模块,导入指定名称的成员(即导入某个标识符)。- 例:
>>> from sys import platform >>> platform 'linux'
- 这种语法的优点:
platform
这样的标识符,比sys.platform
更短,写代码时更方便。
- 这种语法的缺点:
platform
这样的标识符,没有前缀,可能与当前作用域的某个标识符重名。- 代码的可读性差。当读者看到
platform
这样的标识符时,不知道它是当前脚本定义的变量,还是通过 import 导入的。
- 可以同时导入多个成员,用逗号分隔:
>>> from sys import platform, path
- 可以用
import *
导入全部成员。但这样不能看出具体导入了哪些成员,导致代码的可读性很差,建议不要这样做。from sys import *
- 例:
可以用关键字
as
将导入的标识符重命名:import sys as _sys from sys import path as _path
一般将 import 语句写在 Python 脚本的开头,使得导入的标识符,可在全局作用域内访问。
- 也可以将 import 语句写在某个函数或方法内,使得执行该语句块时才会执行 import 语句,并且导入的模块只能在该局部作用域内被访问。
- pyinstaller 在打包 Python 脚本时,会将局部导入的各个模块一起收集,即使该 import 语句位于不会被执行的 if 分支。
# 包
在一个文件目录中,如果存在一个名为
__init__.py
的文件(内容可以为空),则 Python 解释器会将该目录识别为一个包(package)。- 一个包,主要作用是充当一个命名空间。
- 一个包,可以包含多个 Python 模块,作为该包的成员。
- 一个包,可以嵌套包含其它包目录。
- 用
import
语句可以导入一个包,但本质上是导入包中的一个模块。- 执行
import <package>
语句,相当于执行import <package>.__init__
,导入包中一个名为__init__
的模块。 - 因此,每个包必须包含一个名为
__init__.py
的文件。
- 执行
- 一个包,主要作用是充当一个命名空间。
例:假设磁盘中存在以下文件目录
├── mypackage │ ├── __init__.py │ ├── module1.py │ └── module2.py └── test.py
此时 mypackage 会被识别为一个包,可以在 test.py 文件中,执行以下几种 import 语句:
import mypackage import mypackage.module1 from mypackage import module1 from mypackage.module1 import xx
# 寻址
执行
import xx
可以导入某个模块或包,但 Python 解释器如何知道这个模块或包,的实际文件路径?sys.path
的取值为 list 类型,记录了几个常用的目录。Python 解释器会到这些目录及其子目录中,查找名为 xx 的模块或包。- 如果找到了,则导入。如果没找到,则抛出异常:
ModuleNotFoundError: No module named 'xx'
- 例:
>>> sys.path ['', '/usr/lib64/python39.zip', '/usr/lib64/python3.9', ...]
- 用户可以添加其它目录到 sys.path 中:不过每次重启 Python 解释器时,都会重置 sys.path 的值,除非声明终端环境变量
>>> sys.path.append('/tmp')
export PYTHONPATH=/tmp
如果一个脚本位于一个包内,则可以通过相对地址导入其它模块或包:
from .utils import * # 从当前目录导入(需要当前目录是一个包) from .. import * # 从上层目录导入(需要当前目录、上层目录都是包)
- 如果该脚本被间接执行,则 Python 解释器会自动将这些相对地址,转换成绝对地址。
- 如果该脚本被直接执行,则 Python 解释器不能处理相对地址,会报错。
如果模块名、包名会变化,如何导入?可以调用 importlib 模块的 import_module() 函数,输入一个 str 字符串作为模块名或包名,然后导入。
>>> from importlib import import_module >>> m = import_module('os') # 相当于 import os >>> m <module 'os' from '/usr/lib64/python3.6/os.py'> >>> m = import_module('os.path') # 相当于 import os.path >>> m <module 'posixpath' from '/usr/lib64/python3.6/posixpath.py'> >>> m = import_module('sys', package='os') # 相当于 from os import sys >>> m <module 'sys' (built-in)>
# 间接执行
通过 import 导入一个模块时,实际上会执行一遍该模块的全部 Python 代码,然后将其中创建的所有对象,保存到当前脚本的作用域,供当前脚本访问。
- 例如:用 Python 解释器执行 a.py ,而 a.py 导入了 b.py ,则最终两个脚本都会被执行一次,只不过一个是被直接执行,一个是被间接执行。
- 例如,编写一个 test.py :
print('Hello') def fun1(): print('fun1') def fun2(): print('fun2') fun1()
- 然后在终端导入该脚本:
>>> from test import fun2 Hello # 这里间接执行了 print('Hello') >>> fun2() fun2 fun1 # 导入一个函数或类时,其调用的其它内容会被自动导入 >>> fun1() # 但是并不能被直接访问,因为不存在于当前作用域 NameError: name 'fun1' is not defined >>> from test import fun1 # 重复导入同一个模块,不会重复执行它。因此这里不会重复执行 print('Hello') >>> fun1() fun1
- 每个模块只会在第一次导入时,执行其中的 Python 代码。因此,如果修改了某个模块的内容,则需要重启 Python 解释器,才能重新导入该模块。
Python 解释器导入一个模块时,会在该模块的同一目录下,生成一个
__pycache__
文件夹,用于缓存该模块预编译生成的字节码文件(扩展名为 .pyc 或.pyo )。- 当 Python 解释器下一次导入该模块时,会检查该模块文件是否发生变化,没有的话就读取之前的
__pycache__
,从而更快地导入。 __pycache__
中会记录每个模块文件的修改日期,从而判断每个模块文件是否发生变化。
- 当 Python 解释器下一次导入该模块时,会检查该模块文件是否发生变化,没有的话就读取之前的
# 循环导入
- 循环导入,是指两个 Python 模块相互导入,或者多个 Python 模块的导入关系形成一个回路。
- 这种操作是可行的,并不会引发报错。
- 此时相当于将多个模块中的所有 Python 语句,按导入顺序组合成一个大型脚本,可能产生冲突。
- 例:
- 创建两个循环导入的脚本文件:
A.py :B.py :print('start', __name__) import B id = 1 print(B.id) print('end', __name__)
print('start', __name__) import A id = 2 print(A.id) print('end', __name__)
- 执行时输出如下:
[root@CentOS ~]# python3 A.py start __main__ start B start A # 此时 B.py 导入 A.py ,从头执行其中的代码,重复执行了 print('start', __name__) Traceback (most recent call last): File "A.py", line 3, in <module> # 此时 A.py 尚未完成初始化,就导入 B.py ,输出 start B import B File "B.py", line 3, in <module> # 此时 B.py 尚未完成初始化,就导入 A.py ,输出 start A import A File "A.py", line 5, in <module> # 此时在 A.py 中访问 B.id ,但该变量尚未创建,所以报错 print(B.id) AttributeError: partially initialized module 'B' has no attribute 'id' (most likely due to a circular import)
- 将 A.py 中的
print(B.id)
改为print(dir(B))
,就可以循环导入。如下:也可以将 A.py 中的 print(B.id) 放到不会立即执行的代码块中:[root@CentOS ~]# python3 A.py start __main__ start B start A ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__'] end A # 此时 B.py 尚未完成初始化,但导入的 A.py 已经完整执行了一次,完成了初始化 1 # 此时可以在 B.py 中访问 A.id end B ['A', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'id'] end __main__
def test(): print(B.id)
- 创建两个循环导入的脚本文件: