第五单元:工程化实践

掌握异常处理、测试、环境管理等工程化技能,构建可靠的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应用开发!