Python 模块和包
模块是包含 Python 代码的文件,包是组织模块的目录。理解模块和包的工作原理,对于构建可维护的 Python 项目至关重要。
模块基础
什么是模块?
模块是一个 Python 文件(.py),包含函数、类、变量等定义。模块的主要作用是:
- 代码复用:将相关功能组织在一起,方便多处使用
- 命名空间隔离:避免不同模块中的同名冲突
- 可维护性:将大型程序分解为可管理的小文件
创建模块
# mymodule.py
"""这是一个示例模块的文档字符串"""
# 模块级常量
VERSION = "1.0.0"
AUTHOR = "张三"
# 模块级变量
default_greeting = "你好"
def greet(name):
"""向指定的人问好"""
return f"{default_greeting},{name}!"
def add(a, b):
"""两个数相加"""
return a + b
class Calculator:
"""简单计算器类"""
def __init__(self):
self.result = 0
def add(self, num):
"""累加"""
self.result += num
return self.result
def subtract(self, num):
"""累减"""
self.result -= num
return self.result
def reset(self):
"""重置结果"""
self.result = 0
return self.result
# 私有函数(约定俗成)
def _helper_function():
"""内部辅助函数,不应被外部直接调用"""
pass
# 模块级代码(仅在直接运行时执行)
if __name__ == "__main__":
print(greet("世界"))
calc = Calculator()
print(calc.add(10))
导入模块
# 方式1:导入整个模块
import mymodule
print(mymodule.VERSION) # 1.0.0
print(mymodule.greet("李四")) # 你好,李四!
calc = mymodule.Calculator()
# 方式2:导入特定内容
from mymodule import greet, add, Calculator
print(greet("王五"))
print(add(3, 5))
calc = Calculator()
# 方式3:使用别名
import mymodule as mm
from mymodule import Calculator as Calc
print(mm.VERSION)
calc = Calc()
# 方式4:导入所有内容(不推荐)
from mymodule import *
# 问题:污染命名空间,可能导致命名冲突
# 问题:不清楚哪些内容来自哪个模块
导入的最佳实践
# 推荐:明确导入来源
import os
import sys
from collections import Counter, defaultdict
from datetime import datetime
# 不推荐
from os import *
from sys import *
# 导入顺序(PEP 8)
# 1. 标准库
# 2. 第三方库
# 3. 本地模块
# 标准库
import os
import sys
from datetime import datetime
# 第三方库
import requests
import numpy as np
# 本地模块
from myproject.utils import helper
from myproject.models import User
模块搜索路径
Python 按以下顺序查找模块:
- 当前目录
- 环境变量
PYTHONPATH中的目录 - Python 安装目录的标准库
- 第三方包安装目录(site-packages)
import sys
# 查看搜索路径
print(sys.path)
# 动态添加搜索路径
sys.path.insert(0, '/path/to/module')
# 查看模块位置
import os
print(os.__file__) # 模块文件路径
模块的 __name__ 属性
# mymodule.py
def main():
"""主函数"""
print("程序运行中...")
def run():
"""执行函数"""
print("执行任务...")
# 当模块被直接运行时,__name__ 为 "__main__"
# 当模块被导入时,__name__ 为模块名 "mymodule"
if __name__ == "__main__":
main()
else:
# 被导入时执行的代码
print("mymodule 被导入")
# 命令行运行: python mymodule.py
# 输出: 程序运行中...
# 在其他文件中导入
# import mymodule
# 输出: mymodule 被导入
包(Package)
什么是包?
包是一个包含 __init__.py 文件的目录,用于组织多个模块。包可以嵌套,形成层级结构。
创建包
mypackage/
├── __init__.py # 包初始化文件
├── module1.py # 模块1
├── module2.py # 模块2
└── subpackage/ # 子包
├── __init__.py
└── module3.py
__init__.py 文件
__init__.py 文件在包被导入时自动执行,主要用途:
# mypackage/__init__.py
# 1. 包级常量
__version__ = "1.0.0"
__author__ = "张三"
# 2. 导入子模块,简化 API
from .module1 import function1, Class1
from .module2 import function2
# 3. 定义包级函数
def get_info():
"""获取包信息"""
return f"mypackage v{__version__} by {__author__}"
# 4. 初始化代码
print(f"mypackage {__version__} 已加载")
# 5. 定义 __all__ 控制导出内容
__all__ = ['function1', 'Class1', 'function2', 'get_info']
使用示例:
# 直接使用包级别导入的内容
from mypackage import function1, get_info
# 等同于
from mypackage.module1 import function1
# 查看版本
print(mypackage.__version__)
__init__.py 最佳实践
# 好的做法:保持 __init__.py 简洁
# 如果包只是模块的容器,可以留空
# __init__.py 内容为空
# 或者只导入最常用的内容
from .core import main_function
from .utils import helper
__all__ = ['main_function', 'helper']
# 不好的做法:在 __init__.py 中执行耗时操作
# 会导致导入包时变慢
import time
time.sleep(5) # 千万不要这样做!
导入包
# 导入整个包
import mypackage
mypackage.function1()
# 导入子模块
import mypackage.module1
mypackage.module1.function1()
# 从包导入模块
from mypackage import module1
module1.function1()
# 从子包导入
from mypackage.subpackage import module3
# 深层导入
from mypackage.subpackage.module3 import deep_function
# 使用别名简化
import mypackage.subpackage.module3 as m3
相对导入与绝对导入
绝对导入
从项目根目录开始的完整路径导入:
# 假设目录结构:
# project/
# ├── main.py
# └── mypackage/
# ├── __init__.py
# ├── module1.py
# └── subpackage/
# ├── __init__.py
# └── module3.py
# 在 module3.py 中导入 module1
from mypackage.module1 import function1
from mypackage import module2
优点:路径清晰,不易出错 缺点:包名改变时需要修改所有导入
相对导入
使用 . 表示当前包,.. 表示上级包:
# 在 mypackage/subpackage/module3.py 中
# 导入同级模块(当前包内)
from . import sibling_module
# 导入上级包的模块
from ..module1 import function1
from .. import module2
# 导入上级包的上级包
from ... import top_level_module
优点:包可以重命名而不影响内部导入 缺点:只能在包内部使用
相对导入规则
| 语法 | 含义 |
|---|---|
from . import module | 导入当前目录的模块 |
from .module import func | 从当前目录的模块导入 |
from .. import module | 导入上级目录的模块 |
from ..package import module | 从上级目录的包导入 |
from ... import module | 导入上两级目录的模块 |
相对导入常见错误
# 错误:在脚本中直接运行使用了相对导入的模块
# module3.py
from ..module1 import function1 # 相对导入
# 直接运行会报错
# python mypackage/subpackage/module3.py
# ValueError: attempted relative import beyond top-level package
# 解决方案1:使用 -m 运行
# python -m mypackage.subpackage.module3
# 解决方案2:修改导入方式(不推荐)
if __name__ == "__main__":
# 直接运行时使用绝对导入
from mypackage.module1 import function1
else:
# 被导入时使用相对导入
from ..module1 import function1
命名空间包(PEP 420)
什么是命名空间包?
命名空间包允许将一个包分散在多个目录中,不同部分可以独立安装和版本管理。这在大型项目和库分发中非常有用。
与普通包的区别
# 普通包结构
mypackage/
├── __init__.py # 必须存在
├── module_a.py
└── module_b.py
# 命名空间包结构(无 __init__.py)
# 可以分散在多个位置
/path1/mynamespace/ # 无 __init__.py
└── package_a/
├── __init__.py
└── module_a.py
/path2/mynamespace/ # 无 __init__.py
└── package_b/
├── __init__.py
└── module_b.py
创建命名空间包(Python 3.3+)
根据 PEP 420,只需要在命名空间目录中不放置 __init__.py 文件:
# 项目 A:mynamespace-package-a/
pyproject.toml
src/
mynamespace/ # 命名空间包(无 __init__.py)
package_a/ # 普通包
__init__.py
core.py
# 项目 B:mynamespace-package-b/
pyproject.toml
src/
mynamespace/ # 命名空间包(无 __init__.py)
package_b/ # 普通包
__init__.py
utils.py
安装后,两个包共享同一命名空间:
from mynamespace.package_a import core
from mynamespace.package_b import utils
# 两个包独立安装和版本管理
pip install mynamespace-package-a
pip install mynamespace-package-b
pyproject.toml 配置
# mynamespace-package-a/pyproject.toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "mynamespace-package-a"
version = "1.0.0"
[tool.setuptools.packages.find]
where = ["src"]
# 注意:不要包含命名空间目录本身
命名空间包使用场景
-
大型组织:多个团队独立开发和发布包
google/ # 命名空间
cloud/ # google-cloud 包
auth/ # google-auth 包
api/ # google-api 包 -
插件系统:核心包和插件包分开安装
myapp/
core/ # 核心包
plugins/ # 插件命名空间
auth/ # 认证插件
cache/ # 缓存插件
注意事项
# 关键规则:
# 1. 命名空间目录不能有 __init__.py
# 2. 所有共享命名空间的包都必须遵循此规则
# 3. 如果任何一个包有 __init__.py,命名空间会失效
# 错误示例
/path1/mynamespace/
__init__.py # 这会导致问题!
package_a/
...
/path2/mynamespace/
package_b/ # 无法被导入
...
项目结构最佳实践
推荐的项目结构
project_name/
├── pyproject.toml # 项目配置(推荐)
├── README.md # 项目说明
├── LICENSE # 许可证
├── .gitignore # Git 忽略规则
├── src/ # 源代码目录(src layout)
│ └── mypackage/
│ ├── __init__.py
│ ├── core.py
│ ├── utils.py
│ └── subpackage/
│ ├── __init__.py
│ └── module.py
├── tests/ # 测试目录
│ ├── __init__.py
│ ├── conftest.py
│ └── test_core.py
├── docs/ # 文档目录
│ └── index.md
└── scripts/ # 脚本目录
└── deploy.sh
src layout vs flat layout
# Flat layout(简单项目适用)
project/
├── pyproject.toml
├── mypackage/
│ └── __init__.py
└── tests/
# Src layout(推荐,大型项目适用)
project/
├── pyproject.toml
├── src/
│ └── mypackage/
│ └── __init__.py
└── tests/
src layout 的优点:
- 测试必须通过安装包来运行,确保打包正确
- 避免导入歧义
- 与现代打包工具配合良好
模块组织原则
# 按功能划分模块
mypackage/
├── __init__.py # 包初始化,暴露公共 API
├── models.py # 数据模型
├── services.py # 业务逻辑
├── utils.py # 工具函数
├── config.py # 配置管理
├── exceptions.py # 自定义异常
└── constants.py # 常量定义
# __init__.py 中控制导出
from .models import User, Product
from .services import UserService, ProductService
from .exceptions import ValidationError
__all__ = [
'User', 'Product',
'UserService', 'ProductService',
'ValidationError',
]
避免循环导入
# 问题:循环导入
# models.py
from .services import get_user
class User:
def get_data(self):
return get_user(self.id)
# services.py
from .models import User
def get_user(user_id):
return User.query.get(user_id)
# 解决方案1:延迟导入
# models.py
class User:
def get_data(self):
from .services import get_user # 在方法内导入
return get_user(self.id)
# 解决方案2:重构代码结构
# 将共享的内容提取到单独模块
# types.py
from dataclasses import dataclass
@dataclass
class UserData:
id: int
name: str
# models.py
from .types import UserData
class User:
def get_data(self) -> UserData:
...
# services.py
from .types import UserData
def get_user(user_id: int) -> UserData:
...
安装和管理第三方包
使用 pip
# 安装包
pip install requests
pip install requests==2.28.0 # 指定版本
pip install requests>=2.28.0 # 最低版本
pip install "requests>=2.28.0,<3.0.0" # 版本范围
# 从 git 安装
pip install git+https://github.com/user/repo.git
# 从本地安装
pip install ./path/to/package
# 卸载包
pip uninstall requests
# 查看已安装的包
pip list
pip list --outdated # 查看过时的包
# 查看包信息
pip show requests
# 导出依赖
pip freeze > requirements.txt
# 从 requirements.txt 安装
pip install -r requirements.txt
requirements.txt 格式
# requirements.txt
# 生产环境依赖
requests>=2.28.0
flask>=2.0.0
sqlalchemy>=1.4.0
# 使用环境变量
# -r requirements-base.txt
# 注释
# 这是注释
# 从 git 安装
git+https://github.com/user/[email protected]
开发依赖分离
# requirements.txt(生产依赖)
requests>=2.28.0
flask>=2.0.0
# requirements-dev.txt(开发依赖)
-r requirements.txt # 包含生产依赖
pytest>=7.0.0
black>=23.0.0
ruff>=0.1.0
mypy>=1.0.0
虚拟环境
# 创建虚拟环境
python -m venv .venv
# 激活虚拟环境
# Windows
.venv\Scripts\activate
# macOS/Linux
source .venv/bin/activate
# 在虚拟环境中安装包
pip install requests
# 退出虚拟环境
deactivate
常用标准库模块
# datetime - 日期时间处理
from datetime import datetime, timedelta, date
now = datetime.now()
today = date.today()
tomorrow = today + timedelta(days=1)
formatted = now.strftime("%Y-%m-%d %H:%M:%S")
parsed = datetime.strptime("2024-01-01", "%Y-%m-%d")
# json - JSON 处理
import json
data = {"name": "张三", "age": 25}
json_str = json.dumps(data, ensure_ascii=False)
loaded = json.loads(json_str)
# 文件读写
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# os - 操作系统接口
import os
os.getcwd() # 当前工作目录
os.listdir(".") # 列出目录内容
os.makedirs("dir/subdir", exist_ok=True) # 创建目录
os.path.join("dir", "file.txt") # 路径拼接
# pathlib - 面向对象的路径操作(推荐)
from pathlib import Path
p = Path("dir/subdir/file.txt")
p.parent # 父目录
p.name # 文件名
p.suffix # 扩展名
p.exists() # 是否存在
p.read_text() # 读取文本
p.write_text("content") # 写入文本
# re - 正则表达式
import re
text = "我的邮箱是 [email protected]"
match = re.search(r'\w+@\w+\.\w+', text)
if match:
print(match.group())
# 分割和替换
re.split(r'\s+', "a b c") # ['a', 'b', 'c']
re.sub(r'\d+', 'X', "a1b2c3") # 'aXbXcX'
# random - 随机数
import random
random.random() # 0-1 随机浮点数
random.randint(1, 10) # 1-10 随机整数
random.choice(['a', 'b', 'c']) # 随机选择
random.sample(range(100), 10) # 随机采样
random.shuffle(lst) # 原地打乱
# collections - 容器扩展
from collections import Counter, defaultdict, namedtuple, deque
# 计数器
counter = Counter(['a', 'b', 'a', 'c', 'a'])
print(counter['a']) # 3
# 默认字典
dd = defaultdict(list)
dd['key'].append(1) # 不需要检查键是否存在
# 命名元组
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
print(p.x, p.y) # 1 2
# 双端队列
dq = deque([1, 2, 3])
dq.appendleft(0)
dq.pop()
小结
本章我们学习了:
- 模块的创建、导入和
__name__属性 - 包的结构和
__init__.py文件的作用 - 相对导入与绝对导入的区别和使用场景
- 命名空间包(PEP 420)的概念和应用
- 项目结构最佳实践
- 第三方包的安装和管理
- 常用标准库模块
练习
- 创建一个包含多个模块的包,实现一个简单的计算器功能
- 在包中使用
__init__.py暴露公共 API - 创建一个命名空间包,分成两个独立安装的子包
- 编写
requirements.txt和requirements-dev.txt文件 - 使用
pathlib模块实现一个文件查找工具