6 minutes
模块与包管理
到目前为止,我们所有的代码都写在了一个文件中。当程序变得庞大时,把所有代码塞在一个文件里会让维护变得痛苦不堪。
Python 通过**模块(Module)和包(Package)**来解决这个问题。模块就是 .py 文件,包就是包含模块的目录。借助这套机制,你可以把代码拆分成逻辑单元,复用第三方库,并组织自己的项目结构。
模块的概念
一个 .py 文件就是一个模块。模块名就是文件名(去掉 .py)。
# 文件:greeting.py
"""问候模块"""
def say_hello(name):
return f"Hello, {name}!"
def say_goodbye(name):
return f"Goodbye, {name}!"
PI = 3.14159 # 模块中也可以定义变量
print(f"模块 {__name__} 已加载") # 每个模块都有 __name__
在其他文件中导入它:
# 文件:main.py
import greeting # 导入 greeting 模块
print(greeting.say_hello("Alice")) # Hello, Alice!
print(greeting.PI) # 3.14159
import 的各种方式
基本导入
# 方式 1:导入整个模块
import math
print(math.sqrt(16)) # 4.0
# 方式 2:导入特定对象
from math import sqrt, pi
print(sqrt(25)) # 5.0
print(pi) # 3.14159...
# 方式 3:导入并重命名
import numpy as np
from math import sqrt as square_root
print(square_root(100)) # 10.0
# 方式 4:导入所有(谨慎使用!)
from math import * # 可能造成命名冲突
print(sin(0)) # 0.0
import * 的风险
# 假设你有两个模块:
# module_a.py
def fun():
return "A"
# module_b.py
def fun():
return "B"
# 在你的代码中:
from module_a import *
from module_b import *
print(fun()) # "B" —— module_b 的 fun 覆盖了 module_a 的
规则: 避免 from module import *。如果要用,在模块中通过 __all__ 精确控制导出内容。
导入模块时发生了什么?
Python 导入模块时会做三件事:
- 搜索模块(在
sys.path中查找) - 执行模块代码(从上到下运行)
- 缓存模块(
sys.modules中缓存,避免重复加载)
# sys.modules 中可以看到所有已加载的模块
import sys
print("math" in sys.modules) # False——还没导入 math
import math
print("math" in sys.modules) # True——已缓存
sys.path 与模块搜索路径
Python 从哪里寻找模块?答案在 sys.path 中:
import sys
for path in sys.path:
print(path)
典型的 sys.path 包含:
- 当前脚本所在目录(或当前工作目录)
PYTHONPATH环境变量中指定的目录- Python 安装目录中的
site-packages(第三方库所在位置) - Python 标准库目录
添加自定义搜索路径
import sys
# 方法 1:追加到 sys.path
sys.path.append("/my/custom/path")
# 方法 2:使用 PYTHONPATH 环境变量(推荐)
# 在命令行中:
# export PYTHONPATH="/my/custom/path:$PYTHONPATH" # Linux/macOS
# set PYTHONPATH=C:\my\custom\path;%PYTHONPATH% # Windows
# 方法 3:使用 site-packages(通过 pip 安装)
if __name__ == "__main__" 惯用法
这是 Python 中最重要的惯用法之一。它的作用是:当模块被直接执行时才运行某些代码,被导入的时候不运行。
# 文件:calculator.py
def add(a, b):
return a + b
def sub(a, b):
return a - b
# 测试代码——只在直接运行时执行
if __name__ == "__main__":
print("测试 calculator 模块:")
print(f"add(3, 5) = {add(3, 5)}")
print(f"sub(10, 4) = {sub(10, 4)}")
# 文件:main.py
import calculator # 导入时不会触发测试代码
print(calculator.add(1, 2)) # 3
# "测试 calculator 模块:" 不会被打印
原理:
- 当模块被直接执行时,
__name__被设为"__main__" - 当模块被导入时,
__name__被设为模块名(如"calculator")
这个惯用法让每个 .py 文件既可以作为可复用的模块,又可以作为独立的脚本运行。
创建包
包(Package)是一个包含 __init__.py 文件的目录。当 Python 导入包时,会执行 __init__.py 中的代码。
包的结构
myproject/
├── main.py
└── mypackage/
├── __init__.py
├── math_ops.py
└── string_ops.py
# mypackage/__init__.py
print("正在初始化 mypackage 包...")
# 可以在 __init__.py 中导入子模块,方便外部访问
from .math_ops import add, multiply
from .string_ops import reverse
__all__ = ["add", "multiply", "reverse"]
# mypackage/math_ops.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
# mypackage/string_ops.py
def reverse(text):
return text[::-1]
def capitalize(text):
return text.capitalize()
# main.py
import mypackage # 打印 "正在初始化 mypackage 包..."
# 通过 __init__.py 暴露的接口访问
print(mypackage.add(3, 5)) # 8
print(mypackage.reverse("abc")) # cba
# 也可以直接访问子模块
from mypackage import math_ops
print(math_ops.multiply(4, 7)) # 28
__init__.py 的设计哲学
__init__.py 定义了包的公共接口。好的包设计应该:
- 只暴露用户需要的部分
- 隐藏内部实现细节
- 提供便捷的顶层导入
# 好的 __init__.py —— 精选导出
from .core import Engine
from .utils import format_result
from .exceptions import EngineError
__all__ = ["Engine", "format_result", "EngineError"]
# 内部实现细节不导出
# from .internal import _process, _validate # 不暴露
绝对导入 vs 相对导入
绝对导入
从包的根目录开始指定完整路径:
import mypackage.math_ops
from mypackage.math_ops import add
# 在子包中:
from mypackage.subpackage import something
相对导入
相对于当前模块的位置,使用 . 表示当前目录,.. 表示上级目录:
# 在 mypackage/math_ops.py 中
# 导入同级的 string_ops
from .string_ops import reverse
# 导入父级的 utils
from ..utils import format_result
# 导入兄弟包的模块
from ..other_package import helper
重要限制:
- 相对导入只能在包内部使用
- 直接运行包内的模块(
python mypackage/math_ops.py)会导致相对导入失败 - 相对导入的模块不能作为入口脚本运行
# 正确使用方式
python -m mypackage.math_ops # 使用 -m 参数以模块方式运行
绝对导入 vs 相对导入的选择
| 方式 | 优点 | 缺点 |
|---|---|---|
| 绝对导入 | 清晰、稳定、可重命名包 | 包名较长时繁琐 |
| 相对导入 | 简洁、重命名包时无需修改 | 只能用于包内部,容易混淆 |
建议: 大多数情况下使用绝对导入。只有在包内部深层引用时才用相对导入。
__all__ 变量
__all__ 控制 from module import * 的行为,也是一个模块公开 API 的文档:
# 文件:utils.py
__all__ = ["format_date", "parse_email"]
def format_date(date):
return date.strftime("%Y-%m-%d")
def parse_email(email):
return email.split("@")
def _internal_helper(): # 以下划线开头表示内部函数
pass
# 外部使用 from utils import * 时,只能得到 format_date 和 parse_email
# 包级别的 __all__ 在 __init__.py 中
# mypackage/__init__.py
from .math_ops import add, multiply
from .string_ops import reverse
__all__ = ["add", "multiply", "reverse"]
第三方包与 pip
Python 的第三方生态是其最大优势之一。使用 pip 来安装和管理第三方包:
# 安装包
pip install requests
# 安装特定版本
pip install requests==2.31.0
# 升级包
pip install --upgrade requests
# 卸载包
pip uninstall requests
# 查看已安装的包
pip list
# 查看包的信息
pip show requests
requirements.txt
当你需要分享项目依赖时,使用 requirements.txt 文件记录所有依赖:
# requirements.txt
requests==2.31.0
numpy>=1.24.0
pandas<2.0.0
flask>=2.3.0,<3.0.0
# 导出当前环境的依赖
pip freeze > requirements.txt
# 安装所有依赖
pip install -r requirements.txt
虚拟环境
每个项目应该有自己的虚拟环境,避免依赖冲突:
# 创建虚拟环境(Python 3.3+ 内置 venv 模块)
python -m venv venv
# 激活虚拟环境
# Windows:
venv\Scripts\activate
# Linux/macOS:
source venv/bin/activate
# 停用虚拟环境
deactivate
循环导入(Circular Import)
当模块 A 导入了模块 B,而模块 B 又(直接或间接)导入模块 A,就形成了循环导入。这通常会导致 ImportError。
示例
# 文件:a.py
from b import func_b
def func_a():
print("A")
func_b()
# 文件:b.py
from a import func_a
def func_b():
print("B")
func_a()
# 运行 a.py
import a # ImportError: cannot import name 'func_b' from partially initialized module 'b'
解决方案
方案 1:将公共代码提取到第三个模块
# 文件:common.py
# 存放 a 和 b 都依赖的公共代码
方案 2:延迟导入(在函数内部导入)
# a.py
def func_a():
from b import func_b # 在函数内导入,避免导入时立即执行
print("A")
func_b()
方案 3:使用 import 而非 from
# a.py
import b # 导入模块而不是具体的函数
def func_a():
print("A")
b.func_b() # 通过模块名访问
命名空间包(Namespace Package)
命名空间包是 Python 3.3+ 引入的概念。它允许一个包分布在多个目录中,而不需要每个目录都有 __init__.py:
# 目录结构:
# project/
# ├── package/
# │ └── sub_a/
# │ └── module_a.py
# └── extension/
# └── package/
# └── sub_b/
# └── module_b.py
# 只要 package 目录中没有 __init__.py,它们就组成一个命名空间包
import package.sub_a.module_a
import package.sub_b.module_b
命名空间包的主要用途是让一个包的多个部分可以来自不同的地方(例如插件系统)。
标准库精选
Python 自带"电池"——标准库。以下是一些常用标准库模块:
import os # 操作系统接口
import sys # 系统相关功能
import json # JSON 处理
import re # 正则表达式
import math # 数学函数
import random # 随机数
import datetime # 日期和时间
import pathlib # 路径操作(推荐替代 os.path)
import collections # 额外的数据结构
import itertools # 迭代器工具
import functools # 函数式工具
import logging # 日志
import unittest # 单元测试
实战:构建一个小型工具包
综合运用本章知识,创建一个日期处理工具包:
dateutils/
├── __init__.py
├── parsing.py
├── formatting.py
└── calculations.py
# dateutils/__init__.py
"""日期处理工具包"""
from .parsing import parse_date, try_parse
from .formatting import format_iso, format_cn, format_short
from .calculations import days_between, add_days, is_weekend
__all__ = [
"parse_date", "try_parse",
"format_iso", "format_cn", "format_short",
"days_between", "add_days", "is_weekend",
]
# dateutils/parsing.py
from datetime import datetime
DATE_FORMATS = [
"%Y-%m-%d",
"%Y/%m/%d",
"%Y.%m.%d",
"%m-%d-%Y",
]
def parse_date(date_str: str) -> datetime:
"""尝试多种格式解析日期字符串"""
for fmt in DATE_FORMATS:
try:
return datetime.strptime(date_str, fmt)
except ValueError:
continue
raise ValueError(f"无法解析日期: {date_str}")
def try_parse(date_str: str):
"""安全地尝试解析,失败返回 None"""
try:
return parse_date(date_str)
except ValueError:
return None
# dateutils/formatting.py
from datetime import datetime
def format_iso(dt: datetime) -> str:
"""格式化为 ISO 标准"""
return dt.strftime("%Y-%m-%d")
def format_cn(dt: datetime) -> str:
"""格式化为中文格式"""
return f"{dt.year}年{dt.month}月{dt.day}日"
def format_short(dt: datetime) -> str:
"""格式化为短格式"""
return dt.strftime("%m/%d")
# dateutils/calculations.py
from datetime import datetime, timedelta
def days_between(d1: datetime, d2: datetime) -> int:
"""计算两个日期之间的天数差"""
return abs((d2.date() - d1.date()).days)
def add_days(dt: datetime, days: int) -> datetime:
"""增加指定天数"""
return dt + timedelta(days=days)
def is_weekend(dt: datetime) -> bool:
"""判断是否是周末"""
return dt.weekday() >= 5 # 5=周六, 6=周日
# 使用示例
from dateutils import parse_date, format_cn, days_between, is_weekend
d1 = parse_date("2026-05-12")
print(format_cn(d1)) # 2026年5月12日
d2 = parse_date("2026/06/01")
print(f"相差 {days_between(d1, d2)} 天") # 相差 20 天
print(f"是周末吗?{is_weekend(d1)}") # 是周末吗?False
小结
本章我们学习了 Python 的模块和包系统:
- 模块是
.py文件,包是包含__init__.py的目录 import的四种方式:整体导入、从模块导入、重命名导入、通配符导入- 模块搜索路径:
sys.path决定了 Python 去哪里找模块 if __name__ == "__main__":让一个文件既可被导入又可独立运行__init__.py:包的初始化代码和公共接口定义- 绝对导入 vs 相对导入:
from . import和from mypackage import __all__:控制模块的公开接口- 第三方包管理:pip、requirements.txt、虚拟环境
- 循环导入:原因与三种解决方案
- 命名空间包:无
__init__.py的多目录包
下一步: 有了函数和模块的基础,接下来我们来处理实际开发中最常见的任务之一——文件读写操作。
Summary: 模块与包的概念、导入方式、pip 与虚拟环境。