Python代码质量:从规范到自动化检查
Python代码质量从规范到自动化检查1. 技术分析1.1 代码质量维度维度描述工具代码风格PEP 8规范black, isort类型检查类型注解检查mypy代码规范最佳实践flake8, pylint安全检查潜在漏洞bandit, safety测试覆盖代码测试比例coverage1.2 工具对比工具功能性能学习曲线black代码格式化快低flake8代码检查快低mypy类型检查中中pylint全面检查慢高ruff快速linting极快低2. 核心功能实现2.1 代码格式化配置# pyproject.toml [tool.black] line-length 88 target-version [py39, py310, py311] include \.pyi?$ exclude /( \.git | \.venv | build | dist )/ [tool.isort] profile black line_length 88 known_first_party [src] skip [.venv, build, dist] [tool.mypy] python_version 3.9 warn_return_any true warn_unused_configs true disallow_untyped_defs false ignore_missing_imports true [tool.ruff] line-length 88 target-version py39 [tool.ruff.lint] select [E, F, W, I, N, UP, B, C4] ignore [E501] # 行长度由black处理 [tool.coverage.run] source [src] omit [*/tests/*, */test_*.py] [tool.coverage.report] exclude_lines [ pragma: no cover, if __name__ .__main__.:, raise AssertionError(), ]2.2 单元测试实践import pytest from typing import List, Optional class DataValidator: 数据验证器 staticmethod def validate_email(email: str) - bool: 验证邮箱格式 import re pattern r^[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}$ return bool(re.match(pattern, email)) staticmethod def validate_positive(value: float) - bool: 验证正数 return value 0 staticmethod def validate_in_range(value: float, min_val: float, max_val: float) - bool: 验证范围 return min_val value max_val class TestDataValidator: 数据验证器测试 pytest.mark.parametrize(email,expected, [ (testexample.com, True), (user.namedomain.co.uk, True), (invalid-email, False), (domain.com, False), (user, False), (, False), ]) def test_validate_email(self, email, expected): assert DataValidator.validate_email(email) expected pytest.mark.parametrize(value,expected, [ (1.0, True), (0.0, False), (-1.0, False), (100.5, True), ]) def test_validate_positive(self, value, expected): assert DataValidator.validate_positive(value) expected def test_validate_in_range(self): assert DataValidator.validate_in_range(5, 0, 10) True assert DataValidator.validate_in_range(0, 0, 10) True assert DataValidator.validate_in_range(10, 0, 10) True assert DataValidator.validate_in_range(-1, 0, 10) False assert DataValidator.validate_in_range(11, 0, 10) False class TestEdgeCases: 边界情况测试 def test_empty_string(self): assert DataValidator.validate_email() False def test_unicode_email(self): assert DataValidator.validate_email(用户例子.广告) False def test_very_long_email(self): long_email a * 100 example.com # 应该能处理但可能返回False取决于具体实现 result DataValidator.validate_email(long_email) assert isinstance(result, bool)2.3 Mock与测试隔离from unittest.mock import Mock, patch, MagicMock import pytest class APIClient: API客户端 def __init__(self, base_url: str): self.base_url base_url self.session None def fetch(self, endpoint: str) - dict: 获取数据 import requests response requests.get(f{self.base_url}/{endpoint}) return response.json() class TestAPIClient: API客户端测试 patch(requests.get) def test_fetch_success(self, mock_get): 测试成功获取 mock_response Mock() mock_response.json.return_value {status: success, data: [1, 2, 3]} mock_get.return_value mock_response client APIClient(https://api.example.com) result client.fetch(users) assert result[status] success assert result[data] [1, 2, 3] mock_get.assert_called_once_with(https://api.example.com/users) patch(requests.get) def test_fetch_error(self, mock_get): 测试获取失败 mock_get.side_effect ConnectionError(Network error) client APIClient(https://api.example.com) with pytest.raises(ConnectionError): client.fetch(users) def test_with_fixture(self, mock_get): 使用fixture的测试 # fixture在conftest.py中定义 result self.client.fetch(users) assert status in result2.4 性能测试import pytest import time class TestPerformance: 性能测试 def test_sort_performance(self): 测试排序性能 import random # 生成大量数据 data [random.randint(0, 10000) for _ in range(10000)] start time.perf_counter() sorted_data sorted(data) elapsed time.perf_counter() - start # 应该在1秒内完成 assert elapsed 1.0, f排序耗时 {elapsed:.2f}s超过1秒 # 验证排序正确性 assert sorted_data sorted(data) pytest.mark.benchmark def test_list_comprehension_performance(self, benchmark): 基准测试列表推导式 result benchmark(lambda: [i**2 for i in range(10000)]) assert len(result) 10000 # conftest.py def pytest_configure(config): config.addinivalue_line(markers, benchmark: mark test as a benchmark) pytest.fixture def sample_data(): 示例数据fixture return [i for i in range(100)]3. 持续集成配置3.1 pre-commit配置# .pre-commit-config.yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - id: check-merge-conflict - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black language_version: python3.10 - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - id: isort args: [--profile, black] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.0.261 hooks: - id: ruff args: [--fix] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.3.0 hooks: - id: mypy additional_dependencies: [types-all]3.2 GitHub Actions CI# .github/workflows/ci.yml name: CI on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.9, 3.10, 3.11] steps: - uses: actions/checkoutv3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-pythonv4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e .[dev] - name: Lint with ruff run: ruff check src/ - name: Format check with black run: black --check src/ - name: Type check with mypy run: mypy src/ - name: Test with pytest run: | coverage run -m pytest tests/ coverage report --fail-under80 - name: Upload coverage uses: codecov/codecov-actionv3 with: files: ./coverage.xml4. 代码质量指标4.1 覆盖率报告# 运行测试并生成覆盖率报告 $ coverage run -m pytest tests/ $ coverage report -m Name Stmts Miss Cover Missing ----------------------------------------------------- src/validators.py 45 5 89% 23,45,67 src/models.py 78 12 85% 34,56,78 tests/test_validators.py 60 0 100% - ----------------------------------------------------- TOTAL 183 17 91%4.2 复杂度分析# 使用radon进行复杂度分析 from radon.metrics import mi_visit, h_visit from radon.complexity import cc_visit def analyze_complexity(filepath: str): 代码复杂度分析 with open(filepath, r) as f: source f.read() # 圈复杂度 complexity cc_visit(source) print(圈复杂度:) for item in complexity: if item.classname: name f{item.classname}.{item.name} else: name item.name print(f {name}: {item.complexity}) # 维护性指数 mi mi_visit(source, multiTrue) print(f\n维护性指数: {mi:.1f}) # Halstead指标 from radon.metrics import h_visit halstead h_visit(source) print(f难度: {halstead.difficulty:.1f})5. 最佳实践5.1 代码审查清单- [ ] 代码符合PEP 8规范 - [ ] 函数和类有docstring - [ ] 类型注解完整 - [ ] 单元测试覆盖关键逻辑 - [ ] 没有硬编码的魔法数字 - [ ] 错误处理适当 - [ ] 没有安全漏洞 - [ ] 性能符合要求5.2 提交前检查#!/bin/bash # pre-commit-check.sh set -e echo 运行代码检查... # 格式化 black --check src/ echo ✓ 格式化检查通过 # 检查import isort --check-only --diff src/ echo ✓ import检查通过 # Lint ruff check src/ echo ✓ Lint检查通过 # 类型检查 mypy src/ echo ✓ 类型检查通过 # 测试 pytest tests/ -v echo ✓ 测试通过 echo 所有检查通过!6. 总结代码质量保障要点自动化使用pre-commit和CI/CD自动化检查覆盖率保持80%的测试覆盖率持续改进定期审视和改进代码质量