第五单元:工程化实践
掌握异常处理、测试、环境管理等工程化技能,构建可靠的Python项目
预计学习时间:5-6小时
单元介绍
欢迎来到Python基础课程的第五单元!在这个单元中,你将学习工程化实践的核心技能,这些技能对于构建可靠、可维护的Python项目至关重要。
学习目标
- 掌握异常处理的方法
- 学会编写和运行测试
- 理解环境管理的重要性
- 掌握代码规范和静态检查
- 学会使用日志系统
- 了解打包和分发Python包
- 能够构建专业的Python项目
学习提示
💡 重要提示:工程化实践是区分专业开发者和业余开发者的关键。建议你在实际项目中应用这些技能,培养良好的编码习惯和工程意识!
5.1 异常处理:让程序更健壮
异常处理是处理程序运行时错误的重要机制,它可以使程序更加健壮。
基本异常处理
使用try-except语句来捕获和处理异常:
# 基本异常处理
try:
# 可能会引发异常的代码
num = int(input("请输入一个数字: "))
result = 10 / num
print(f"结果: {result}")
except ValueError:
# 处理值错误
print("错误: 请输入有效的数字")
except ZeroDivisionError:
# 处理除零错误
print("错误: 除数不能为零")
except Exception as e:
# 处理其他所有异常
print(f"发生错误: {e}")
finally:
# 无论是否发生异常都会执行的代码
print("程序执行完毕")
# 异常的层次结构
# BaseException
# ├── SystemExit
# ├── KeyboardInterrupt
# └── Exception
# ├── ArithmeticError
# │ ├── ZeroDivisionError
# │ └── OverflowError
# ├── ValueError
# ├── TypeError
# ├── FileNotFoundError
# └── ...
自定义异常
创建自定义异常类来处理特定的错误情况:
# 自定义异常类
class NegativeNumberError(Exception):
"""负数错误"""
pass
class AgeError(Exception):
"""年龄错误"""
def __init__(self, message, age):
self.message = message
self.age = age
super().__init__(self.message)
# 使用自定义异常
def calculate_square_root(number):
"""计算平方根"""
if number < 0:
raise NegativeNumberError("不能计算负数的平方根")
import math
return math.sqrt(number)
def check_age(age):
"""检查年龄"""
if age < 0:
raise AgeError("年龄不能为负数", age)
elif age > 150:
raise AgeError("年龄不能超过150", age)
return "年龄有效"
# 测试自定义异常
try:
result = calculate_square_root(-4)
print(f"平方根: {result}")
except NegativeNumberError as e:
print(f"错误: {e}")
try:
result = check_age(-5)
print(result)
except AgeError as e:
print(f"错误: {e.message}, 输入的年龄: {e.age}")
try:
result = check_age(200)
print(result)
except AgeError as e:
print(f"错误: {e.message}, 输入的年龄: {e.age}")
异常处理的最佳实践
# 最佳实践1: 只捕获特定的异常
# 不推荐
try:
# 代码
pass
except:
# 捕获所有异常,包括系统退出等
print("发生错误")
# 推荐
try:
# 代码
pass
except (ValueError, TypeError) as e:
# 只捕获特定异常
print(f"发生错误: {e}")
# 最佳实践2: 合理使用finally
# 例如,确保文件关闭
try:
file = open("example.txt", "r")
content = file.read()
print(content)
except FileNotFoundError:
print("文件不存在")
finally:
# 无论是否发生异常,都会关闭文件
if 'file' in locals():
file.close()
# 更优雅的方式:使用with语句
with open("example.txt", "r") as file:
try:
content = file.read()
print(content)
except FileNotFoundError:
print("文件不存在")
# with语句会自动关闭文件
# 最佳实践3: 适当使用raise重新抛出异常
def process_data(data):
"""处理数据"""
try:
# 处理数据
if not data:
raise ValueError("数据不能为空")
return data.upper()
except ValueError as e:
# 记录错误
print(f"处理数据时出错: {e}")
# 重新抛出异常
raise
# 测试
try:
result = process_data("")
print(result)
except ValueError as e:
print(f"调用者捕获到错误: {e}")
5.2 测试:确保代码质量
测试是确保代码质量的重要手段,Python提供了多种测试框架。
使用unittest
unittest是Python的标准测试库:
import unittest
# 要测试的函数
def add(a, b):
"""加法函数"""
return a + b
def divide(a, b):
"""除法函数"""
if b == 0:
raise ZeroDivisionError("除数不能为零")
return a / b
# 测试类
class TestMathFunctions(unittest.TestCase):
"""测试数学函数"""
def test_add(self):
"""测试加法函数"""
self.assertEqual(add(1, 2), 3)
self.assertEqual(add(-1, 1), 0)
self.assertEqual(add(0, 0), 0)
def test_divide(self):
"""测试除法函数"""
self.assertEqual(divide(6, 3), 2)
self.assertEqual(divide(5, 2), 2.5)
# 测试异常
with self.assertRaises(ZeroDivisionError):
divide(1, 0)
if __name__ == '__main__':
unittest.main()
# 运行测试的方法
# 1. 直接运行文件
# python test_math.py
# 2. 使用unittest命令
# python -m unittest test_math.py
# 3. 发现并运行所有测试
# python -m unittest discover
使用pytest
pytest是一个流行的第三方测试框架,提供了更简洁的语法:
# 安装pytest
# pip install pytest
# 要测试的函数
def add(a, b):
"""加法函数"""
return a + b
def divide(a, b):
"""除法函数"""
if b == 0:
raise ZeroDivisionError("除数不能为零")
return a / b
# 测试函数
def test_add():
"""测试加法函数"""
assert add(1, 2) == 3
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_divide():
"""测试除法函数"""
assert divide(6, 3) == 2
assert divide(5, 2) == 2.5
def test_divide_by_zero():
"""测试除以零的情况"""
with pytest.raises(ZeroDivisionError):
divide(1, 0)
# 运行测试的方法
# python -m pytest test_math.py -v
# 测试发现
# python -m pytest
# 测试覆盖率
# pip install pytest-cov
# python -m pytest --cov=my_module tests/
测试的最佳实践
# 最佳实践1: 测试应该独立
# 避免测试之间的依赖
def test_first():
# 不要修改全局状态
pass
def test_second():
# 不要依赖test_first的结果
pass
# 最佳实践2: 测试应该全面
# 测试正常情况和边界情况
def test_add():
# 正常情况
assert add(1, 2) == 3
# 边界情况
assert add(0, 0) == 0
assert add(-1, 1) == 0
assert add(1000000, 1000000) == 2000000
# 最佳实践3: 使用测试夹具(fixtures)
import pytest
@pytest.fixture
def sample_data():
"""提供测试数据"""
return [1, 2, 3, 4, 5]
def test_sum(sample_data):
"""测试求和"""
assert sum(sample_data) == 15
def test_average(sample_data):
"""测试平均值"""
assert sum(sample_data) / len(sample_data) == 3
# 最佳实践4: 模拟(Mock)外部依赖
from unittest.mock import Mock, patch
def get_data_from_api():
"""从API获取数据"""
# 实际代码会调用外部API
import requests
response = requests.get("https://api.example.com/data")
return response.json()
def test_get_data_from_api():
"""测试从API获取数据"""
with patch('requests.get') as mock_get:
# 配置mock对象
mock_get.return_value.json.return_value = {"data": "test"}
# 调用函数
result = get_data_from_api()
# 断言
assert result == {"data": "test"}
mock_get.assert_called_once_with("https://api.example.com/data")
5.3 环境管理:隔离项目依赖
环境管理是Python项目的重要组成部分,它可以帮助你隔离不同项目的依赖。
使用venv
venv是Python 3.3+内置的虚拟环境工具:
# 创建虚拟环境
# python -m venv venv
# 激活虚拟环境
# Windows: venv\Scripts\activate
# macOS/Linux: source venv/bin/activate
# 安装包
# pip install requests
# 查看已安装的包
# pip list
# 导出依赖
# pip freeze > requirements.txt
# 从requirements.txt安装依赖
# pip install -r requirements.txt
# 退出虚拟环境
# deactivate
# 删除虚拟环境
# rm -rf venv # macOS/Linux
# rmdir /s venv # Windows
使用conda
conda是一个流行的包和环境管理工具:
# 创建环境
# conda create -n myenv python=3.10
# 激活环境
# conda activate myenv
# 安装包
# conda install requests
# 或使用pip
# pip install requests
# 查看环境
# conda env list
# 查看已安装的包
# conda list
# 导出环境
# conda env export > environment.yml
# 从environment.yml创建环境
# conda env create -f environment.yml
# 退出环境
# conda deactivate
# 删除环境
# conda env remove -n myenv
依赖管理的最佳实践
# 最佳实践1: 为每个项目创建独立的虚拟环境
# 避免不同项目的依赖冲突
# 最佳实践2: 使用requirements.txt或environment.yml管理依赖
# 示例requirements.txt
# requests==2.31.0
# pandas==2.1.0
# pytest==7.4.0
# 最佳实践3: 固定依赖版本
# 避免因依赖版本变化导致的问题
# 好的做法
# requests==2.31.0
# 不好的做法
# requests
# 最佳实践4: 使用pip-tools管理依赖
# 安装pip-tools
# pip install pip-tools
# 创建requirements.in文件
# requests
# pandas
# pytest
# 生成requirements.txt
# pip-compile requirements.in
# 升级依赖
# pip-compile --upgrade requirements.in
# 安装依赖
# pip-sync requirements.txt
# 最佳实践5: 使用Poetry
# 安装Poetry
# curl -sSL https://install.python-poetry.org | python3 -
# 初始化项目
# poetry init
# 安装依赖
# poetry add requests
# 运行命令
# poetry run python script.py
# 构建包
# poetry build
5.4 代码规范:保持代码质量
代码规范是保持代码质量和可读性的重要手段。
PEP 8
PEP 8是Python的官方代码风格指南:
# PEP 8 示例
# 缩进:使用4个空格
# 好的做法
def function():
if True:
print("Hello")
# 不好的做法
def function():
if True:
print("Hello")
# 行长度:每行不超过79个字符
# 好的做法
long_variable_name = "This is a long string that should be wrapped to the next line"
# 不好的做法
long_variable_name = "This is a long string that should be wrapped to the next line but it's not, which violates PEP 8"
# 空行:函数之间空2行,函数内逻辑块之间空1行
# 好的做法
def function1():
pass
def function2():
if True:
print("Hello")
print("World")
# 命名规范:
# - 函数和变量:小写字母,用下划线分隔
# - 类:首字母大写,驼峰命名
# - 常量:全大写,用下划线分隔
# 好的做法
def calculate_area(length, width):
pass
class Rectangle:
pass
MAX_ITERATIONS = 100
# 不好的做法
def CalculateArea(Length, Width):
pass
class rectangle:
pass
maxIterations = 100
使用linting工具
使用工具来检查代码是否符合规范:
# 安装工具
# pip install flake8 pylint black
# 使用flake8检查代码
# flake8 my_script.py
# 使用pylint检查代码
# pylint my_script.py
# 使用black自动格式化代码
# black my_script.py
# 配置文件
# .flake8
"""
[flake8]
max-line-length = 88
extend-ignore = E203, W503
"""
# pyproject.toml
"""
[tool.black]
line-length = 88
target-version = ['py310']
[tool.pylint.main]
enable = ["unused-imports"]
disable = ["C0111"]
"""
# 集成到IDE
# VS Code: 安装Python扩展,配置flake8和black
# PyCharm: 内置支持
5.5 日志管理:记录程序运行状态
日志是程序运行状态的重要记录,它可以帮助你调试和监控程序。
使用logging模块
Python的logging模块提供了灵活的日志记录功能:
import logging
# 配置日志
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
filename='app.log',
filemode='w'
)
# 创建logger
logger = logging.getLogger(__name__)
# 不同级别的日志
logger.debug('这是调试信息')
logger.info('这是信息')
logger.warning('这是警告')
logger.error('这是错误')
logger.critical('这是严重错误')
# 日志级别
# DEBUG < INFO < WARNING < ERROR < CRITICAL
# 实际应用示例
def divide(a, b):
"""除法函数"""
try:
result = a / b
logger.info(f"成功计算 {a} / {b} = {result}")
return result
except ZeroDivisionError as e:
logger.error(f"除零错误: {e}")
raise
# 测试
try:
divide(10, 2)
divide(10, 0)
except ZeroDivisionError:
pass
高级日志配置
import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
# 创建logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 创建文件处理器(大小轮转)
file_handler = RotatingFileHandler(
'app.log',
maxBytes=1024*1024, # 1MB
backupCount=5
)
file_handler.setLevel(logging.DEBUG)
# 创建时间轮转处理器
time_handler = TimedRotatingFileHandler(
'app.log',
when='midnight',
interval=1,
backupCount=7
)
time_handler.setLevel(logging.DEBUG)
# 创建格式化器
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# 设置格式化器
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
time_handler.setFormatter(formatter)
# 添加处理器到logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# 测试
logger.debug('调试信息')
logger.info('信息')
logger.warning('警告')
logger.error('错误')
logger.critical('严重错误')
# 日志配置文件
# logging.conf
"""
[DEFAULT]
handlers = console, file
level = DEBUG
[handlers]
keys = console, file
[handler_console]
class = StreamHandler
level = INFO
formatter = simpleFormatter
args = (sys.stderr,)
[handler_file]
class = FileHandler
level = DEBUG
formatter = simpleFormatter
args = ('app.log', 'w')
[formatters]
keys = simpleFormatter
[formatter_simpleFormatter]
format = %(asctime)s - %(name)s - %(levelname)s - %(message)s
"""
# 加载配置文件
# logging.config.fileConfig('logging.conf')
5.6 打包与分发:分享你的代码
打包和分发是将你的Python代码分享给他人的重要步骤。
项目结构
一个标准的Python项目结构:
# 项目结构示例
my_project/
├── my_package/
│ ├── __init__.py
│ ├── module1.py
│ └── module2.py
├── tests/
│ ├── __init__.py
│ ├── test_module1.py
│ └── test_module2.py
├── setup.py
├── setup.cfg
├── pyproject.toml
├── README.md
└── LICENSE
# __init__.py 文件
# my_package/__init__.py
"""我的包"""
__version__ = "1.0.0"
# 导出模块
from .module1 import function1
from .module2 import function2
__all__ = ["function1", "function2"]
使用setup.py
使用setup.py来定义包的信息:
# setup.py
from setuptools import setup, find_packages
setup(
name="my-package",
version="1.0.0",
description="我的Python包",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
author="Your Name",
author_email="your.email@example.com",
url="https://github.com/yourusername/my-package",
packages=find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires=">=3.7",
install_requires=[
"requests>=2.31.0",
"pandas>=2.0.0",
],
extras_require={
"dev": [
"pytest>=7.0.0",
"flake8>=6.0.0",
"black>=23.0.0",
],
},
entry_points={
"console_scripts": [
"my-command=my_package.module1:main",
],
},
)
# 构建包
# python setup.py sdist bdist_wheel
# 安装包
# pip install .
# 从本地安装
# pip install ./dist/my_package-1.0.0.tar.gz
# 上传到PyPI
# 安装twine
# pip install twine
# 上传
# twine upload dist/*
使用pyproject.toml
现代Python项目使用pyproject.toml:
# pyproject.toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "1.0.0"
description = "我的Python包"
readme = "README.md"
authors = [
{ name = "Your Name", email = "your.email@example.com" },
]
license = { file = "LICENSE" }
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
requires-python = ">=3.7"
dependencies = [
"requests>=2.31.0",
"pandas>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"flake8>=6.0.0",
"black>=23.0.0",
]
[project.scripts]
my-command = "my_package.module1:main"
# 构建包
# python -m build
# 安装包
# pip install .
# 上传到PyPI
# twine upload dist/*
5.7 单元练习题
太棒了!你已经学完了第五单元的所有内容。现在让我们通过一些练习来巩固所学知识吧!
练习题1:异常处理
编写一个函数,计算一个数的平方根,处理负数和非数字输入的情况。
查看参考答案
def calculate_square_root(number):
"""计算平方根"""
try:
number = float(number)
if number < 0:
raise ValueError("不能计算负数的平方根")
import math
return math.sqrt(number)
except ValueError as e:
print(f"错误: {e}")
return None
except TypeError:
print("错误: 输入必须是数字或可转换为数字的字符串")
return None
# 测试
print(calculate_square_root(4)) # 2.0
print(calculate_square_root(-1)) # 错误
print(calculate_square_root("9")) # 3.0
print(calculate_square_root("abc")) # 错误
练习题2:测试
为上面的calculate_square_root函数编写测试用例。
查看参考答案
import unittest
import math
from my_module import calculate_square_root
class TestCalculateSquareRoot(unittest.TestCase):
"""测试计算平方根函数"""
def test_positive_number(self):
"""测试正数"""
self.assertEqual(calculate_square_root(4), 2.0)
self.assertEqual(calculate_square_root(9), 3.0)
self.assertAlmostEqual(calculate_square_root(2), math.sqrt(2))
def test_zero(self):
"""测试零"""
self.assertEqual(calculate_square_root(0), 0.0)
def test_negative_number(self):
"""测试负数"""
self.assertIsNone(calculate_square_root(-1))
self.assertIsNone(calculate_square_root(-4))
def test_string_number(self):
"""测试字符串形式的数字"""
self.assertEqual(calculate_square_root("4"), 2.0)
self.assertEqual(calculate_square_root("9"), 3.0)
def test_invalid_input(self):
"""测试无效输入"""
self.assertIsNone(calculate_square_root("abc"))
self.assertIsNone(calculate_square_root(None))
self.assertIsNone(calculate_square_root([]))
if __name__ == '__main__':
unittest.main()
练习题3:环境管理
创建一个虚拟环境,安装requests和pandas包,并导出依赖到requirements.txt文件。
查看参考答案
# 步骤1: 创建虚拟环境
# python -m venv venv
# 步骤2: 激活虚拟环境
# Windows: venv\Scripts\activate
# macOS/Linux: source venv/bin/activate
# 步骤3: 安装包
# pip install requests pandas
# 步骤4: 导出依赖
# pip freeze > requirements.txt
# 步骤5: 查看requirements.txt内容
# cat requirements.txt
# 示例requirements.txt内容
# pandas==2.1.0
# python-dateutil==2.8.2
# pytz==2023.3
# requests==2.31.0
# six==1.16.0
# urllib3==2.0.4
练习题4:日志管理
编写一个函数,使用logging模块记录函数的执行情况。
查看参考答案
import logging
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
filename='app.log',
filemode='a'
)
logger = logging.getLogger(__name__)
def divide(a, b):
"""除法函数"""
logger.info(f"开始计算 {a} / {b}")
try:
result = a / b
logger.info(f"计算成功,结果: {result}")
return result
except ZeroDivisionError as e:
logger.error(f"除零错误: {e}")
raise
finally:
logger.info("计算完成")
# 测试
try:
divide(10, 2)
divide(10, 0)
except ZeroDivisionError:
pass
# 查看日志文件
# cat app.log
练习题5:项目结构
创建一个简单的Python项目结构,包含包、测试和配置文件。
查看参考答案
# 创建项目目录结构
# mkdir -p my_calculator/{calculator,tests}
# 创建文件
# calculator/__init__.py
"""计算器包"""
__version__ = "1.0.0"
from .operations import add, subtract, multiply, divide
__all__ = ["add", "subtract", "multiply", "divide"]
# calculator/operations.py
def add(a, b):
"""加法"""
return a + b
def subtract(a, b):
"""减法"""
return a - b
def multiply(a, b):
"""乘法"""
return a * b
def divide(a, b):
"""除法"""
if b == 0:
raise ZeroDivisionError("除数不能为零")
return a / b
# tests/__init__.py
# 空文件
# tests/test_operations.py
import unittest
from calculator.operations import add, subtract, multiply, divide
class TestOperations(unittest.TestCase):
"""测试操作函数"""
def test_add(self):
"""测试加法"""
self.assertEqual(add(1, 2), 3)
self.assertEqual(add(-1, 1), 0)
def test_subtract(self):
"""测试减法"""
self.assertEqual(subtract(5, 3), 2)
self.assertEqual(subtract(3, 5), -2)
def test_multiply(self):
"""测试乘法"""
self.assertEqual(multiply(2, 3), 6)
self.assertEqual(multiply(-2, 3), -6)
def test_divide(self):
"""测试除法"""
self.assertEqual(divide(6, 3), 2)
self.assertEqual(divide(5, 2), 2.5)
with self.assertRaises(ZeroDivisionError):
divide(1, 0)
if __name__ == '__main__':
unittest.main()
# setup.py
from setuptools import setup, find_packages
setup(
name="my-calculator",
version="1.0.0",
description="简单的计算器包",
packages=find_packages(),
python_requires=">=3.7",
)
# README.md
"""# My Calculator
一个简单的计算器包。
## 安装
```bash
pip install .
```
## 使用
```python
from calculator import add, subtract, multiply, divide
print(add(1, 2))
print(subtract(5, 3))
print(multiply(2, 3))
print(divide(6, 3))
```
"""
# 运行测试
# python -m unittest discover
# 构建包
# python setup.py sdist bdist_wheel
单元总结
🎉 恭喜你完成了第五单元的学习!
- 你掌握了异常处理的方法
- 你学会了编写和运行测试
- 你理解了环境管理的重要性
- 你掌握了代码规范和静态检查
- 你学会了使用日志系统
- 你了解了打包和分发Python包
- 你能够构建专业的Python项目
继续加油,下一个单元我们将学习AI应用开发!