跳到主要内容

Python 模块和包

模块是包含 Python 代码的文件,包是组织模块的目录。理解模块和包的工作原理,对于构建可维护的 Python 项目至关重要。

模块基础

什么是模块?

模块是一个 Python 文件(.py),包含函数、类、变量等定义。模块的主要作用是:

  1. 代码复用:将相关功能组织在一起,方便多处使用
  2. 命名空间隔离:避免不同模块中的同名冲突
  3. 可维护性:将大型程序分解为可管理的小文件

创建模块

# 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 按以下顺序查找模块:

  1. 当前目录
  2. 环境变量 PYTHONPATH 中的目录
  3. Python 安装目录的标准库
  4. 第三方包安装目录(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"]
# 注意:不要包含命名空间目录本身

命名空间包使用场景

  1. 大型组织:多个团队独立开发和发布包

    google/          # 命名空间
    cloud/ # google-cloud 包
    auth/ # google-auth 包
    api/ # google-api 包
  2. 插件系统:核心包和插件包分开安装

    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 的优点:

  1. 测试必须通过安装包来运行,确保打包正确
  2. 避免导入歧义
  3. 与现代打包工具配合良好

模块组织原则

# 按功能划分模块
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()

小结

本章我们学习了:

  1. 模块的创建、导入和 __name__ 属性
  2. 包的结构和 __init__.py 文件的作用
  3. 相对导入与绝对导入的区别和使用场景
  4. 命名空间包(PEP 420)的概念和应用
  5. 项目结构最佳实践
  6. 第三方包的安装和管理
  7. 常用标准库模块

练习

  1. 创建一个包含多个模块的包,实现一个简单的计算器功能
  2. 在包中使用 __init__.py 暴露公共 API
  3. 创建一个命名空间包,分成两个独立安装的子包
  4. 编写 requirements.txtrequirements-dev.txt 文件
  5. 使用 pathlib 模块实现一个文件查找工具

延伸阅读