Python 模块系统:多文件协同与包管理
摘要
模块系统核心概念:
├── 模块 = .py 文件
├── 包 = 目录 + __init__.py
├── import 五种方式(推荐 import 模块名)
├── __name__ == "__main__"(模块自测)
└── 项目结构(src/包名/模块.py)
建议:
- 先学会拆分两个文件,再逐步引入包结构
- 善用
if __name__ == "__main__"编写模块自测代码 - 避免
from xxx import * - 遇到循环导入,说明模块职责划分需要重新设计
第 1 章:为什么需要多文件?
1.1 单文件的局限
想象你写了一个学生管理系统,所有代码都在一个文件中:
# student_system.py(1000+ 行)
def add_student(): ... # 添加学生
def delete_student(): ... # 删除学生
def update_student(): ... # 更新学生
def query_student(): ... # 查询学生
def save_to_file(): ... # 文件操作
def load_from_file(): ... # 文件操作
def print_menu(): ... # 界面显示
def main(): ... # 主程序
# 还有工具函数、常量定义、类定义...
问题:
- 文件过长,查找代码需要大量滚动
- 多人协作时同一文件频繁产生冲突
- 想复用文件操作功能,只能复制粘贴
1.2 多文件的优势
student_system/
├── main.py # 程序入口
├── models.py # 数据模型(学生类)
├── storage.py # 文件存储
├── ui.py # 界面交互
└── utils.py # 工具函数
- 逻辑分离,每个文件职责单一
storage.py可以在其他项目中直接复用- 团队协作,每人负责不同文件
- 可以单独测试每个模块
第 2 章:模块基础
2.1 什么是模块?
模块就是一个 .py 文件。
# math_utils.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
PI = 3.14159
# main.py
import math_utils
result = math_utils.add(2, 3)
print(math_utils.PI)
2.2 import 的五种方式
方式 1:import 模块名(推荐)
import math_utils
print(math_utils.add(1, 2))
print(math_utils.PI)
命名空间清晰,一眼可知函数来自哪个模块。
方式 2:from 模块 import 名称
from math_utils import add, PI
print(add(1, 2))
print(PI)
代码更简洁,适合频繁使用的少量名称。
方式 3:from 模块 import *(不推荐)
from math_utils import *
print(add(1, 2))
print(multiply(3, 4))
print(PI)
方式 4:导入别名
import math_utils as mu
from math_utils import add as addition
print(mu.add(1, 2))
print(addition(1, 2))
适用场景:模块名过长,或避免与内置名称冲突。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
方式 5:相对导入(包内使用)
from . import module # 同级目录
from .. import parent # 上级目录
from .utils import helper # 同级子模块
相对导入只能在包内部使用,直接运行文件(python module.py)会报错,必须用 python -m package.module 方式运行。
2.3 模块搜索路径
当执行 import xxx 时,Python 按以下顺序查找:
- 当前目录(运行脚本所在目录)
PYTHONPATH环境变量中的目录- 标准库目录
- 第三方包目录(site-packages)
import sys
print(sys.path) # 查看完整搜索路径列表
添加自定义路径:
import sys
sys.path.append('/path/to/your/modules')
import my_module
第 3 章:创建自己的模块
3.1 最简单的模块
my_project/
├── greeting.py
└── main.py
# greeting.py
def say_hello(name):
return f"你好,{name}!"
def say_goodbye(name):
return f"再见,{name}!"
if __name__ == "__main__":
print(say_hello("世界"))
print("这是 greeting 模块的测试输出")
# main.py
import greeting
message = greeting.say_hello("小明")
print(message)
python main.py
# 输出:你好,小明!
3.2 __name__ == "__main__" 的作用
这是模块系统中最重要的约定之一。
规则:
- 文件被直接运行时,
__name__的值是"__main__" - 文件被导入时,
__name__的值是模块名
# my_module.py
print(f"模块名: {__name__}")
if __name__ == "__main__":
print("直接运行")
else:
print("被导入")
# 直接运行
python my_module.py
# 输出:模块名: __main__
# 直接运行
# 作为模块导入
python -c "import my_module"
# 输出:模块名: my_module
# 被导入
用途:在 if __name__ == "__main__": 块内编写测试代码,导入时不会执行:
# calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b
if __name__ == "__main__":
assert add(2, 3) == 5
assert divide(10, 2) == 5.0
print("所有测试通过")
3.3 模块只执行一次
Python 会缓存已导入的模块,重复导入不会重新执行模块代码:
# config.py
print("config 模块已加载")
DB_HOST = "localhost"
# main.py
import config # 输出:config 模块已加载
import config # 无输出(从缓存中取)
import config # 无输出
print(config.DB_HOST)
第 4 章:从模块到包
4.1 什么是包?
包是包含 __init__.py 文件的目录,用于组织多个模块。
mypackage/
├── __init__.py
├── module_a.py
├── module_b.py
└── subpackage/
├── __init__.py
└── module_c.py
4.2 __init__.py 的作用
# mypackage/__init__.py
# 控制对外暴露的名称
__all__ = ['func_a', 'func_b', 'ClassA']
# 简化导入路径:让使用者可以直接 from mypackage import func_a
from .module_a import func_a
from .module_b import ClassB
# 包级别初始化
PACKAGE_VERSION = "1.0.0"
import mypackage
mypackage.func_a()
print(mypackage.PACKAGE_VERSION)
4.3 包的多层级导入
# 方式1:导入子包
from mypackage import subpackage
subpackage.module_c.some_func()
# 方式2:导入深层模块
from mypackage.subpackage import module_c
module_c.some_func()
# 方式3:直接导入具体函数
from mypackage.subpackage.module_c import some_func
some_func()
4.4 相对导入 vs 绝对导入
绝对导入(推荐):
from mypackage.module_a import func_a
from mypackage.subpackage.module_c import MyClass
相对导入(包内使用):
# mypackage/module_b.py
from .module_a import func_a # 同级模块
from . import module_a # 导入整个同级模块
from ..other_package import thing # 上级包
from .subpackage import module_c # 子包
第 5 章:项目结构规范
5.1 按规模选择结构
阶段 1:单文件(< 200 行)
hello.py
阶段 2:主程序 + 工具模块(200–500 行)
myproject/
├── main.py
├── utils.py
└── config.py
阶段 3:功能模块化(500–2000 行)
myproject/
├── main.py
├── requirements.txt
├── core/
│ ├── __init__.py
│ ├── models.py
│ ├── database.py
│ └── business.py
├── utils/
│ ├── __init__.py
│ ├── helpers.py
│ └── validators.py
└── config.py
5.2 中型项目标准结构
my_project/
├── README.md
├── LICENSE
├── requirements.txt
├── setup.py
├── .gitignore
│
├── src/
│ └── my_project/
│ ├── __init__.py
│ ├── __main__.py # python -m my_project 入口
│ ├── cli.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── models.py
│ │ └── engine.py
│ ├── utils/
│ │ ├── __init__.py
│ │ └── helpers.py
│ └── config/
│ ├── __init__.py
│ └── settings.py
│
├── tests/
│ ├── __init__.py
│ ├── test_core.py
│ └── test_utils.py
│
└── docs/
└── index.md
5.3 命名规范
| 类型 | 规范 | 示例 |
|---|---|---|
| 模块名 | 小写,可用下划线 | my_module.py、string_utils.py |
| 包名 | 小写,简短 | core、utils |
| 类名 | 大驼峰 | StudentManager |
| 函数名 | 小写,下划线分隔 | calculate_score |
| 常量 | 大写,下划线分隔 | MAX_SIZE = 100 |
| 私有名称 | 下划线前缀 | _internal_func、_private_var |
常见命名错误:
不要用 Python 关键字:for.py, class.py, return.py
不要与标准库同名:random.py, json.py, sys.py
不要有大写或连字符:MyModule.py, my-module.py
推荐:my_module.py, data_processor.py
第 6 章:高级话题
6.1 循环导入问题
# a.py
from b import func_b
def func_a():
return "A"
# b.py
from a import func_a
def func_b():
return "B"
运行会报错:ImportError: cannot import name 'func_a'
解决方案:
方案 1:重构代码(推荐)
将共享代码提取到第三个模块:
common.py <- 放共享函数
a.py <- 导入 common
b.py <- 导入 common
方案 2:延迟导入
# a.py
def func_a():
from b import func_b # 在使用时才导入
return func_b() + "A"
方案 3:合并模块
如果两个模块高度耦合,考虑合并为一个文件。
6.2 __all__ 控制导出接口
# mymodule.py
__all__ = ['public_func', 'PublicClass']
def public_func(): # 外部可用
pass
def _private_func(): # 约定内部使用,不在 __all__ 中
pass
class PublicClass: # 外部可用
pass
from mymodule import *
public_func() # 可以调用
_private_func() # NameError:未被导入(未在 __all__ 中列出)
6.3 动态导入
import importlib
module_name = "math"
math = importlib.import_module(module_name)
print(math.sqrt(16)) # 4.0
应用场景:插件系统、根据配置加载不同后端实现。
def load_backend(name):
module = importlib.import_module(f"backends.{name}")
return module.Database()
db = load_backend("sqlite") # 加载 backends/sqlite.py
db = load_backend("mysql") # 加载 backends/mysql.py