01-模块基础
Python 3.11+
本章讲解 Python 模块的基本概念、导入语法和模块属性。
概念铺垫
1.1 什么是模块
实际场景
你写了一个计算工具 calculator.py,里面有加法、减法等函数。现在你想在另一个项目 invoice.py 里使用这些函数,不想重复写代码。
问题:如何让多个项目共享同一份代码?
概念说明
概念说明
在 Python 中,一个 .py 文件就是一个模块。
模块的名字就是去掉了 .py 后缀的文件名。模块里面可以包含变量、函数、类,甚至可以包含可执行的代码。
┌─────────────────────────────────────────────────────────────┐
│ 模块 = 文件 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 文件:math_utils.py │
│ 模块名:math_utils │
│ │
│ 文件:my_package/helpers.py │
│ 模块名:my_package.helpers │
│ │
└─────────────────────────────────────────────────────────────┘模块的作用:
┌─────────────────────────────────────────────────────────────┐
│ 模块的作用 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 代码组织 │
│ 将相关功能放在同一文件中,便于管理 │
│ │
│ 2. 代码复用 │
│ 一个模块可以被多个程序导入使用 │
│ │
│ 3. 命名空间隔离 │
│ 每个模块有独立的命名空间,避免命名冲突 │
│ │
│ 4. 维护方便 │
│ 修改模块代码,所有导入该模块的程序自动受益 │
│ │
└─────────────────────────────────────────────────────────────┘模块的类型:
| 类型 | 说明 | 示例 |
|---|---|---|
| 内置模块 | Python 内置,无需安装 | sys, os, math |
| 标准库模块 | Python 自带,需导入 | datetime, json, re |
| 第三方模块 | 需要安装 | requests, numpy |
| 自定义模块 | 用户自己编写 | my_module.py |
模块的执行过程
导入时发生了什么
┌─────────────────────────────────────────────────────────────┐
│ 模块导入执行过程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 搜索模块 │
│ 在 sys.path 中查找模块文件 │
│ │
│ 2. 编译模块 │
│ 如果 .pyc 不存在或过期,编译为字节码 │
│ │
│ 3. 执行模块 │
│ 执行模块中的所有顶层代码 │
│ │
│ 4. 创建模块对象 │
│ 创建模块对象,存入 sys.modules │
│ │
│ 5. 绑定名称 │
│ 将模块名绑定到当前命名空间 │
│ │
└─────────────────────────────────────────────────────────────┘模块的搜索路径(sys.path)
当你输入 import xxx 时,Python 会按照 sys.path 列表中的路径顺序去查找:
python
import sys
print(sys.path)搜索顺序:
┌─────────────────────────────────────────────────────────────┐
│ sys.path 搜索顺序 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 当前执行脚本所在的目录 │
│ │
│ 2. 环境变量 PYTHONPATH 中指定的目录 │
│ │
│ 3. Python 的标准库安装目录 │
│ │
│ 4. 第三方库目录(通常是 site-packages) │
│ │
└─────────────────────────────────────────────────────────────┘模块的缓存机制
为了提高性能,Python 在一次运行中只会导入同一个模块一次。
python
# module.py
print("模块被加载")
# main.py
import module # 输出:模块被加载
import module # 无输出(模块已缓存)sys.modules 字典:
python
import sys
# 查看已加载的模块
print(sys.modules.keys())
# 检查模块是否已加载
if 'math' in sys.modules:
print("math 模块已加载")
# 模块对象存储在 sys.modules 中
import math
print(sys.modules['math'] is math) # TrueL1 理解层:会用
import 语法
基本导入方式
Python 提供多种导入方式,可根据需要选择。
python
# 方式 1:导入整个模块
import math
print(math.sqrt(16)) # 4.0
print(math.pi) # 3.14159...
# 方式 2:导入模块中的特定对象
from math import sqrt, pi
print(sqrt(16)) # 4.0
print(pi) # 3.14159...
# 方式 3:导入模块中的所有内容(不推荐)
from math import *
print(sqrt(16)) # 可能造成命名冲突
# 方式 4:使用别名
import math as m
print(m.sqrt(16)) # 4.0
from math import sqrt as square_root
print(square_root(16)) # 4.0import vs from 的本质区别
用"盒子"比喻理解
┌─────────────────────────────────────────────────────────────┐
│ import math:导入整个"盒子" │
├─────────────────────────────────────────────────────────────┤
│ │
│ import math │
│ │
│ 你的代码空间 math 模块(盒子) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ │ │ sqrt() │ │
│ │ math ──────────┼──────────►│ pi │ │
│ │ │ │ e │ │
│ │ │ │ sin() │ │
│ │ │ │ cos() │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ 使用方式:math.sqrt()、math.pi │
│ 必须通过 math 这个"把手"去拿里面的东西 │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ from math import sqrt:只拿"一件东西" │
├─────────────────────────────────────────────────────────────┤
│ │
│ from math import sqrt │
│ │
│ 你的代码空间 math 模块(盒子) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ sqrt() ◄────────┼───────────│ sqrt() │ │
│ │ │ │ pi │ │
│ │ │ │ e │ │
│ │ │ │ sin() │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ 使用方式:sqrt() │
│ sqrt 直接放在你的代码空间里,不需要"把手" │
│ │
└─────────────────────────────────────────────────────────────┘命名空间的区别
python
# ========== import math ==========
import math
# 当前命名空间只有一个名字:math
print(dir()) # [..., 'math']
# 使用时必须加前缀
print(math.sqrt(16)) # 通过 math 找到 sqrt
print(math.pi) # 通过 math 找到 pi
# ========== from math import sqrt ==========
from math import sqrt
# 当前命名空间有一个名字:sqrt(不是 math)
print(dir()) # [..., 'sqrt']
# 直接使用,不需要前缀
print(sqrt(16)) # 直接调用
# 注意:pi 不可用!因为没有导入
# print(pi) # NameError: name 'pi' is not defined冲突问题演示
python
# ========== 场景:两个模块都有同名函数 ==========
# 方式 1:import(不会冲突)
import math
import cmath # 复数数学库
# 两个 sqrt 通过不同的前缀区分
print(math.sqrt(4)) # 2.0(实数平方根)
print(cmath.sqrt(-4)) # 2j(复数平方根)
# ✅ 清晰知道用的是哪个模块的 sqrt
# 方式 2:from(会冲突!)
from math import sqrt
from cmath import sqrt # ⚠️ 覆盖了上一个 sqrt!
print(sqrt(4)) # 用的是 cmath.sqrt
print(sqrt(-4)) # 用的是 cmath.sqrt
# ❌ 第一个 sqrt 被覆盖了,容易出错内存角度理解
python
import math
# 1. 加载整个 math 模块到内存
# 2. 在当前命名空间创建变量 math,指向模块对象
# 3. 通过 math.xxx 访问模块内容
from math import sqrt
# 1. 加载整个 math 模块到内存(模块都会完整加载)
# 2. 在当前命名空间创建变量 sqrt,指向 math.sqrt
# 3. 直接通过 sqrt 访问,不需要前缀
# 验证:两种方式加载的模块是同一个
import math
from math import sqrt
print(sqrt is math.sqrt) # True,指向同一个函数对象命名空间是什么?
命名空间就是一个名字到对象的映射字典。 Python 中每个作用域都有自己的命名空间,导入时只是在当前命名空间里"登记"一个新名字。
┌─────────────────────────────────────────────────┐
│ 内置命名空间(built-in) │
│ print, len, range, int ... │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ 全局命名空间(模块级) │ │
│ │ import math → {'math': <module>} │ │
│ │ from math import sqrt → {'sqrt': <func>} │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ 局部命名空间(函数内) │ │ │
│ │ │ def foo(): │ │ │
│ │ │ x = 1 → {'x': 1} │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘用 globals() 可以直接查看当前全局命名空间的内容:
python
import math
print(globals())
# {..., 'math': <module 'math' from '...'>}
# 命名空间里登记了名字 'math',对应模块对象
from math import sqrt
print(globals())
# {..., 'sqrt': <built-in function sqrt>}
# 命名空间里登记了名字 'sqrt',直接对应函数对象两种导入方式的本质区别只在于"登记了什么名字",模块本身都完整加载进了内存:
| 导入方式 | 命名空间登记的名字 | 访问路径 |
|---|---|---|
import math | math → 模块对象 | math → 模块 → .sqrt → 函数 |
from math import sqrt | sqrt → 函数对象 | sqrt → 函数(少一次属性查找) |
扩展阅读: 命名空间与作用域的完整规则(LEGB)见
02-核心编程篇/01-函数/03-变量作用域.md。
选择建议
┌─────────────────────────────────────────────────────────────┐
│ 什么时候用 import,什么时候用 from? │
├─────────────────────────────────────────────────────────────┤
│ │
│ 推荐用 import 的场景: │
│ ✓ 需要用模块中的多个功能 │
│ ✓ 模块名较短(如 os, sys, re) │
│ ✓ 多人协作的大型项目 │
│ ✓ 不确定是否有命名冲突 │
│ │
│ 示例: │
│ import os │
│ os.path.join('a', 'b') │
│ os.listdir('.') │
│ os.makedirs('dir') │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ 推荐用 from 的场景: │
│ ✓ 只需要模块中的一两个功能 │
│ ✓ 函数名很长或使用频繁 │
│ ✓ 函数名足够明确,不会混淆 │
│ │
│ 示例: │
│ from datetime import datetime │
│ now = datetime.now() # 比 datetime.datetime.now() 简洁 │
│ │
│ from collections import defaultdict │
│ d = defaultdict(list) # 比 collections.defaultdict 简洁 │
│ │
└─────────────────────────────────────────────────────────────┘导入方式的对比总结
| 方式 | 命名空间 | 使用方式 | 冲突风险 | 推荐场景 |
|---|---|---|---|---|
import math | 只有 math | math.sqrt() | 低 | 大型项目、多用模块功能 |
from math import sqrt | 只有 sqrt | sqrt() | 中 | 只需少量功能 |
from math import * | 所有公开名称 | 直接用 | 高 | ❌ 不推荐 |
import math as m | 只有 m | m.sqrt() | 低 | 模块名较长 |
导入顺序规范
按照 PEP 8 规范,导入应分组并排序:
python
# 标准库
import os
import sys
from datetime import datetime
from pathlib import Path
# 第三方库
import numpy as np
import pandas as pd
import requests
# 本地模块
from myproject import utils
from myproject.models import User
from myproject.views import home规范要点:
- 三组之间用空行分隔
- 每组内按字母顺序排列
- 先
import后from ... import - 避免使用
from module import *
模块属性
__name__ 变量
__name__ 变量用于判断模块是被直接运行还是被导入。
python
# my_module.py
def main() -> None:
print("程序主逻辑执行")
if __name__ == '__main__':
main()
else:
print("模块被导入")运行结果:
bash
# 直接运行
python my_module.py
# 输出:程序主逻辑执行
# 作为模块导入
python -c "import my_module"
# 输出:模块被导入常见用法:
python
# my_module.py
def add(a: float, b: float) -> float:
"""加法运算"""
return a + b
def test() -> None:
"""测试函数"""
assert add(1, 2) == 3
assert add(-1, 1) == 0
print("所有测试通过!")
if __name__ == '__main__':
test()其他模块属性
python
# my_module.py
"""
这是模块的文档字符串
用于说明模块的功能
"""
def func() -> None:
pass
# 查看模块属性
import my_module
print(my_module.__name__) # 'my_module'(模块名)
print(my_module.__doc__) # 模块文档字符串
print(my_module.__file__) # 模块文件路径
print(my_module.__dict__) # 模块的命名空间字典
print(my_module.__package__) # 所属包名添加自定义搜索路径
python
import sys
# 动态添加搜索路径
sys.path.append('/your/custom/path')
# 或添加到开头(优先搜索)
sys.path.insert(0, '/your/custom/path')
# 现在可以导入自定义路径下的模块
import my_module解决 ModuleNotFoundError:
python
# 遇到 ModuleNotFoundError 时,首先检查:
# 1. 模块是否在当前目录
# 2. 模块是否在 sys.path 中
# 3. 模块名是否正确(区分大小写)
# 4. 是否安装了第三方模块(pip install)重新加载模块
python
import importlib
import my_module
# 修改了 my_module.py 的代码后,重新加载
importlib.reload(my_module)
# 注意:reload 不会影响已经导入的对象
from my_module import func
importlib.reload(my_module) # func 仍然是旧版本控制导出内容(all)
__all__ 变量用于控制 from module import * 的行为。
python
# my_module.py
__all__ = ['public_func', 'PUBLIC_VAR']
PUBLIC_VAR: str = "public"
_PRIVATE_VAR: str = "private"
def public_func() -> None:
"""公开函数"""
pass
def _private_func() -> None:
"""私有函数(下划线开头)"""
pass
def secret_func() -> None:
"""不在 __all__ 中的函数"""
pass使用效果:
python
# main.py
from my_module import *
print(PUBLIC_VAR) # "public"
public_func() # 可用
# print(_PRIVATE_VAR) # NameError(下划线开头)
# _private_func() # NameError
# secret_func() # NameError(不在 __all__ 中)all 的作用:
┌─────────────────────────────────────────────────────────────┐
│ __all__ 的作用 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 控制 from module import * 的导出内容 │
│ │
│ 2. 明确模块的公开 API │
│ │
│ 3. 文档工具(如 pydoc)会参考 __all__ │
│ │
│ 注意: │
│ • __all__ 只影响 import * │
│ • from module import name 仍然可以导入任意名称 │
│ • 下划线开头的名称默认不会被 import * 导入 │
│ │
└─────────────────────────────────────────────────────────────┘L2 实践层:用好
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
首选 import module | 避免命名冲突,代码意图清晰 | import math 而非 from math import * |
| 按 PEP 8 排序导入 | 便于维护,快速定位问题 | 标准库 → 第三方 → 本地,每组内字母排序 |
用 if __name__ == "__main__" 保护测试代码 | 模块被导入时不执行测试逻辑 | if __name__ == "__main__": test() |
用 __all__ 声明公开 API | 明确模块的公开接口 | __all__ = ["func_a", "MyClass"] |
| 避免模块顶层副作用代码 | 导入时避免意外行为(网络请求、文件写入等) | 将初始化逻辑放入函数中 |
需要多个功能时用 import module | 命名空间隔离,冲突风险最低 | import os,使用 os.path.join() |
反模式:不要这样做
python
# ❌ 使用 from module import * — 污染命名空间
from math import *
print(sqrt(16)) # sqrt 来自哪里?不清楚
# ✅ 显式导入或使用模块前缀
import math
print(math.sqrt(16))
# ---
# ❌ 导入顺序混乱 — 难维护
from myapp.models import User
import os
import requests
from datetime import datetime
# ✅ 按规范排序:标准库 → 第三方 → 本地
import os
from datetime import datetime
import requests
from myapp.models import User
# ---
# ❌ 顶层副作用 — 导入即执行
# bad_module.py
print("正在连接数据库...") # 导入时立即执行!
conn = connect_to_database() # 可能失败,阻塞导入
# ✅ 延迟到调用时执行
# good_module.py
_cache: dict | None = None
def get_connection():
"""按需连接数据库"""
global _cache
if _cache is None:
_cache = connect_to_database()
return _cache
# ---
# ❌ 在模块顶层执行耗时操作
# slow_module.py
result = expensive_computation() # 每次导入都要等待
# ✅ 惰性求值
# fast_module.py
def get_result():
return expensive_computation()适用场景
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 大型项目多人协作 | import module | 命名空间隔离,避免冲突 |
| 只需模块中一两个函数 | from module import func | 减少输入,代码更简洁 |
| 模块名较长 | import long_name as ln | 保持简洁同时避免冲突 |
| 编写库/框架的公开 API | 设置 __all__ | 明确接口,文档友好 |
| 模块同时用于脚本和库 | if __name__ == "__main__" | 兼顾直接运行和导入使用 |
| 需要重新加载模块(开发时) | importlib.reload() | 避免重启 Python 进程 |
L3 专家层:深入
Python 如何实现
CPython 中 import 语句的执行流程:
┌──────────────────────────────────────────────────────────────┐
│ import 语句执行流程 │
├──────────────────────────────────────────────────────────────┤
│ │
│ import math │
│ │ │
│ ▼ │
│ __import__("math", globals(), locals(), [], 0) │
│ │ │
│ ▼ │
│ importlib.__import__() │
│ │ │
│ ├── 1. 检查 sys.modules["math"] 是否已存在 │
│ │ ├─ 存在 → 直接返回缓存对象 │
│ │ └─ 不存在 → 继续查找 │
│ │ │
│ ├── 2. 遍历 sys.meta_path 查找 Finder │
│ │ ├─ BuiltinImporter → 内置模块(如 sys) │
│ │ ├─ FrozenImporter → 冻结模块 │
│ │ └─ PathFinder → 文件系统模块 │
│ │ │ │
│ │ └── 遍历 sys.path_hooks │
│ │ └── 遍历 sys.path 的每个目录 │
│ │ │
│ ├── 3. Finder 返回 Loader + Spec │
│ │ │
│ ├── 4. Loader 执行: │
│ │ ├─ 获取源码 │
│ │ ├─ 编译为字节码(存入 __pycache__/*.pyc) │
│ │ └─ exec(code, module.__dict__) │
│ │ │
│ └── 5. 将模块对象存入 sys.modules │
│ │
└──────────────────────────────────────────────────────────────┘验证代码:
python
import sys
import importlib
# 查看所有已缓存的模块
print(f"已加载模块数: {len(sys.modules)}")
print(f"math 已加载: {'math' in sys.modules}")
# 查看 sys.meta_path(Finder 链)
for i, finder in enumerate(sys.meta_path):
print(f"meta_path[{i}]: {type(finder).__name__}")
# 查看 sys.path_hooks
for i, hook in enumerate(sys.path_hooks):
print(f"path_hooks[{i}]: {hook}")
# 模拟 import 过程
spec = importlib.util.find_spec("math")
print(f"math 的 Spec: {spec}")
print(f" name: {spec.name}")
print(f" loader: {spec.loader}")
print(f" origin: {spec.origin}") # .py 文件路径
print(f" cached: {spec.cached}") # .pyc 文件路径pycache 字节码
┌──────────────────────────────────────────────────────────────┐
│ .pyc 文件结构 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 当 import math 时: │
│ │
│ 1. 查找 __pycache__/math.cpython-311.pyc │
│ 2. 如果 .pyc 存在且比 math.py 新 → 直接加载 .pyc │
│ 3. 如果 .pyc 不存在或已过期 → 编译 math.py → 写入 .pyc │
│ │
│ .pyc 文件包含: │
│ ┌─────────────────────────────────┐ │
│ │ Magic Number (4 bytes) │ 版本标识 │
│ │ Flags (4 bytes) │ 编译标记 │
│ │ Timestamp / Hash (取决于 flag) │ 源文件识别 │
│ │ Source Size (4 bytes) │ 源码大小 │
│ │ Code Object (marshal 序列化) │ 实际字节码 │
│ └─────────────────────────────────┘ │
│ │
│ 字节码缓存策略(PEP 3147 / PEP 552): │
│ - Python 3.7 及更早:基于时间戳判断是否过期 │
│ - Python 3.12+:默认使用确定性哈希(PEP 552) │
│ - 可通过 -B 或 PYTHONDONTWRITEBYTECODE 禁用 .pyc │
│ │
└──────────────────────────────────────────────────────────────┘验证字节码:
python
import dis
import importlib
# 查看 import 语句的字节码
import_module_code = compile("import math", "<test>", "exec")
dis.dis(import_module_code)
# 输出:
# 0 LOAD_CONST 0 (0)
# 2 LOAD_CONST 1 (None)
# 4 IMPORT_NAME 0 (math) ← import 操作码
# 6 STORE_NAME 0 (math)
# 查看 from import 的字节码
from_import_code = compile("from math import sqrt", "<test>", "exec")
dis.dis(from_import_code)
# 0 LOAD_CONST 0 (0)
# 2 LOAD_CONST 1 (('sqrt',))
# 4 IMPORT_NAME 0 (math) ← 同为 IMPORT_NAME
# 6 IMPORT_FROM 1 (sqrt) ← 额外的属性查找
# 8 STORE_NAME 1 (sqrt)
# 10 POP_TOP
# 检查 .pyc 文件
import pathlib
spec = importlib.util.find_spec("math")
pyc_path = pathlib.Path(spec.cached)
print(f"__pycache__ 路径: {spec.cached}")
print(f".pyc 存在: {pyc_path.exists()}")
print(f".pyc 大小: {pyc_path.stat().st_size} bytes")性能考量
| 操作 | 复杂度 | 说明 |
|---|---|---|
首次 import module | O(n·m),遍历路径 + 编译 + 执行 | n = sys.path 条目数,m = 文件系统操作 |
重复 import module | O(1) 字典查找 | 直接从 sys.modules 返回缓存 |
from module import name | O(1) + 属性查找 | 比 import module 多一次模块对象属性查找 |
from module import * | O(k) 遍历 + 绑定 | k = 导出名称数量 |
sys.path.append() | O(1) 列表追加 | 但影响后续所有 import 的查找范围 |
importlib.reload() | O(重新编译 + 重新执行) | 不走 sys.modules 缓存,强制重新加载 |
知识关联
┌──────────────────────────────────────────────────────────────┐
│ 模块基础 知识关联图 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 本章(模块基础) │
│ │ │
│ ├── import 语法 ────────► 04-导入机制(绝对/相对导入) │
│ │ │
│ ├── sys.path ───────────► 02-自定义模块(搜索路径详解) │
│ │ │
│ ├── sys.modules ────────► 04-导入机制(importlib 内部) │
│ │ │
│ ├── __pycache__ 字节码 ─► 性能优化 / importlib 机制 │
│ │ │
│ ├── __name__ ───────────► 02-自定义模块(主模块机制) │
│ │ │
│ ├── __all__ ────────────► 03-包的结构(包级导出控制) │
│ │ │
│ └── 模块作为对象 ────────► 02-自定义模块(模块对象内部) │
│ │
└──────────────────────────────────────────────────────────────┘本章小结
┌─────────────────────────────────────────────────────────────┐
│ 模块基础 知识要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 模块概念: │
│ ✓ 模块是包含 Python 代码的 .py 文件 │
│ ✓ 作用:代码组织、复用、命名空间隔离 │
│ │
│ import 语法: │
│ ✓ import module │
│ ✓ from module import name │
│ ✓ import module as alias │
│ ✓ 导入顺序:标准库 → 第三方 → 本地 │
│ │
│ 模块属性: │
│ ✓ __name__:判断运行方式 │
│ ✓ __doc__:文档字符串 │
│ ✓ __file__:文件路径 │
│ │
│ 导入过程: │
│ ✓ 搜索 → 编译 → 执行 → 缓存 │
│ ✓ 模块只导入一次,可用 reload 强制重载 │
│ │
│ 搜索路径(sys.path): │
│ ✓ 当前目录 → PYTHONPATH → 标准库 → site-packages │
│ ✓ 可动态添加自定义路径 │
│ │
│ 缓存机制: │
│ ✓ sys.modules 存储已加载模块 │
│ ✓ importlib.reload() 重新加载模块 │
│ │
│ 导出控制: │
│ ✓ __all__ 控制 import * 的行为 │
│ ✓ 下划线开头的名称默认不导出 │
│ │
└─────────────────────────────────────────────────────────────┘