python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python代码质量

Python代码质量之从规范到自动化检查全过程

作者:牧码人王木木

本文介绍了提高Python代码质量的十个关键方面,包括清晰的命名、一致性、注释、模块化、错误处理、测试、代码简洁、版本控制、性能优化和文档编写,需要的朋友可以参考下

1. 技术分析

1.1 代码质量维度

维度描述工具
代码风格PEP 8规范black, isort
类型检查类型注解检查mypy
代码规范最佳实践flake8, pylint
安全检查潜在漏洞bandit, safety
测试覆盖代码测试比例coverage

1.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", [
        ("test@example.com", True),
        ("user.name@domain.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 result

2.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/checkout@v3
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v4
        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-under=80
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage.xml

4. 代码质量指标

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, multi=True)
    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. 总结

代码质量保障要点:

  1. 自动化:使用pre-commit和CI/CD自动化检查
  2. 覆盖率:保持80%+的测试覆盖率
  3. 持续改进:定期审视和改进代码质量

到此这篇关于Python代码质量之从规范到自动化检查全过程的文章就介绍到这了,更多相关Python代码质量内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文