强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Python 编程教程 / 25 - 最佳实践

第 25 章:最佳实践

汇总 Python 开发中的代码规范、设计模式、常见陷阱和项目组织经验。


25.1 代码规范

25.1.1 PEP 8 核心规则

规则说明示例
缩进4 个空格不使用 Tab
行长度79(严格)/ 88(Black)使用换行或括号
命名-变量snake_caseuser_name
命名-类PascalCaseUserProfile
命名-常量UPPER_SNAKE_CASEMAX_RETRY
命名-私有前导下划线_internal
空行顶层函数/类之间 2 行类内方法之间 1 行
导入顺序标准库 → 第三方 → 本地使用 isort/ruff

25.1.2 清洁代码原则

# ✅ 使用有意义的变量名
user_age = 25
max_retry_count = 3

# ❌ 不清晰的命名
a = 25
x = 3

# ✅ 函数只做一件事
def calculate_total(items: list[dict]) -> float:
    return sum(item["price"] * item["quantity"] for item in items)

# ❌ 函数做了太多事
def process_order(order):
    # 验证 + 计算 + 发邮件 + 记录日志 + ... 全部塞在一起
    ...

# ✅ 使用 Early Return 减少嵌套
def validate_user(user: dict) -> bool:
    if not user.get("name"):
        return False
    if not user.get("email"):
        return False
    if user["age"] < 0:
        return False
    return True

# ❌ 深层嵌套
def validate_user(user: dict) -> bool:
    if user.get("name"):
        if user.get("email"):
            if user["age"] >= 0:
                return True
    return False

25.2 Pythonic 惯用法

25.2.1 常用 Pythonic 写法

# 1. 解包赋值
a, b = b, a
first, *rest = [1, 2, 3, 4]

# 2. 列表推导
squares = [x**2 for x in range(10)]

# 3. 字典推导
inverted = {v: k for k, v in original.items()}

# 4. 生成器表达式
total = sum(x**2 for x in range(1_000_000))

# 5. walrus 运算符(Python 3.8+)
if (n := len(data)) > 10:
    print(f"数据量 {n} 过大")

# 6. 上下文管理器
with open("file.txt") as f:
    content = f.read()

# 7. enumerate 替代手动索引
for i, item in enumerate(items):
    print(f"{i}: {item}")

# 8. zip 并行遍历
for name, score in zip(names, scores):
    print(f"{name}: {score}")

# 9. 使用 get 安全取值
value = my_dict.get("key", "default")

# 10. 使用 setdefault
my_dict.setdefault("key", []).append(value)

# 11. 链式比较
if 0 < x < 100:
    pass

# 12. any/all
if any(word in text for word in keywords):
    pass

if all(x > 0 for x in numbers):
    pass

25.2.2 避免的写法

# ❌ 用 == 比较 None
if x == None: ...

# ✅ 用 is
if x is None: ...

# ❌ 用 type() 检查类型
if type(x) is list: ...

# ✅ 用 isinstance()
if isinstance(x, list): ...

# ❌ 用 len() 检查空容器
if len(my_list) == 0: ...

# ✅ 直接布尔判断
if not my_list: ...

# ❌ 用 try/except 替代条件判断
try:
    value = my_dict["key"]
except KeyError:
    value = default

# ✅ 使用 get()
value = my_dict.get("key", default)

# ❌ 手动实现内置功能
total = 0
for x in items:
    total += x

# ✅ 使用内置函数
total = sum(items)

25.3 常见陷阱

25.3.1 可变默认参数

# ❌ 经典陷阱
def append_to(item, target=[]):
    target.append(item)
    return target

append_to(1)  # [1]
append_to(2)  # [1, 2] ← 共享同一个列表!

# ✅ 正确做法
def append_to(item, target=None):
    if target is None:
        target = []
    target.append(item)
    return target

25.3.2 闭包中的变量绑定

# ❌ 陷阱
funcs = [lambda: i for i in range(5)]
print([f() for f in funcs])  # [4, 4, 4, 4, 4]

# ✅ 使用默认参数捕获
funcs = [lambda i=i: i for i in range(5)]
print([f() for f in funcs])  # [0, 1, 2, 3, 4]

25.3.3 浮点数精度

# ❌ 浮点数比较
0.1 + 0.2 == 0.3  # False

# ✅ 使用 math.isclose
import math
math.isclose(0.1 + 0.2, 0.3)  # True

# ✅ 金融计算用 Decimal
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") == Decimal("0.3")  # True

25.3.4 作用域陷阱

x = 10

def foo():
    print(x)  # ❌ UnboundLocalError
    x = 20    # 局部赋值导致 x 在整个函数中被当作局部变量

# ✅ 使用 global 或 nonlocal
def foo():
    global x
    print(x)  # 10
    x = 20

25.3.5 类型转换陷阱

# ❌ 链式比较可能出错
x = 5
if 1 < x < 10:  # ✅ 正确
    pass

# ❌ 字符串 join 时注意类型
numbers = [1, 2, 3]
# ",".join(numbers)  # TypeError

# ✅ 需要先转换
",".join(str(n) for n in numbers)  # "1,2,3"

25.4 设计模式

25.4.1 单例模式

from functools import cache

@cache
def get_config():
    return load_config()

# 或使用类
class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

25.4.2 工厂模式

from typing import Protocol

class Serializer(Protocol):
    def serialize(self, data: dict) -> str: ...

class JSONSerializer:
    def serialize(self, data: dict) -> str:
        import json
        return json.dumps(data)

class YAMLSerializer:
    def serialize(self, data: dict) -> str:
        import yaml
        return yaml.dump(data)

def get_serializer(format: str) -> Serializer:
    serializers = {"json": JSONSerializer, "yaml": YAMLSerializer}
    cls = serializers.get(format)
    if cls is None:
        raise ValueError(f"Unknown format: {format}")
    return cls()

25.4.3 策略模式

from typing import Callable

def process_data(data: list, strategy: Callable) -> list:
    return [strategy(item) for item in data]

# 不同策略
uppercase = str.upper
lowercase = str.lower
strip = str.strip

words = [" hello ", " WORLD "]
print(process_data(words, uppercase))
print(process_data(words, strip))

25.4.4 依赖注入

from dataclasses import dataclass, field

@dataclass
class EmailService:
    smtp_host: str = "localhost"

    def send(self, to: str, subject: str, body: str) -> None:
        print(f"Sending email to {to}")

@dataclass
class UserService:
    email_service: EmailService = field(default_factory=EmailService)

    def register(self, email: str) -> None:
        # 业务逻辑
        self.email_service.send(email, "Welcome", "Welcome aboard!")

# 测试时注入 Mock
class MockEmailService:
    def send(self, to, subject, body):
        pass

service = UserService(email_service=MockEmailService())

25.5 项目组织

25.5.1 推荐目录结构

myproject/
├── .github/
│   └── workflows/
│       └── ci.yml
├── src/
│   └── myproject/
│       ├── __init__.py
│       ├── main.py
│       ├── config.py
│       ├── models/
│       │   ├── __init__.py
│       │   └── user.py
│       ├── services/
│       │   ├── __init__.py
│       │   └── user_service.py
│       └── api/
│           ├── __init__.py
│           └── routes.py
├── tests/
│   ├── conftest.py
│   └── test_user_service.py
├── docs/
├── .env.example
├── .gitignore
├── pyproject.toml
├── README.md
└── Makefile

25.5.2 模块划分原则

分层架构:
API 层 → Service 层 → Repository 层 → Database

api/routes.py      # 路由、请求验证、响应格式
services/          # 业务逻辑
models/            # 数据模型
repositories/      # 数据访问
config.py          # 配置

25.6 类型安全

25.6.1 类型注解最佳实践

from typing import TypeVar, Generic, Protocol
from collections.abc import Sequence

# 函数签名
def process(items: Sequence[int]) -> list[str]:
    return [str(x) for x in items]

# 泛型
T = TypeVar("T")

def first(items: Sequence[T]) -> T | None:
    return items[0] if items else None

# Protocol(结构化类型)
class Renderable(Protocol):
    def render(self) -> str: ...

def render_all(items: Sequence[Renderable]) -> str:
    return "\n".join(item.render() for item in items)

# TypedDict
from typing import TypedDict

class UserData(TypedDict):
    name: str
    age: int
    email: str | None

25.6.2 运行时类型检查

from pydantic import BaseModel, Field, field_validator

class CreateUser(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    email: str
    age: int = Field(..., ge=0, le=150)

    @field_validator("email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("Invalid email")
        return v.lower()

# 自动验证
user = CreateUser(name="Alice", email="[email protected]", age=30)

25.7 文档与注释

25.7.1 Docstring 规范

def calculate_shipping(
    weight: float,
    distance: float,
    express: bool = False,
) -> float:
    """计算运费。

    根据重量和距离计算运费,支持普通和快递两种方式。

    Args:
        weight: 包裹重量,单位为千克,必须大于 0。
        distance: 运输距离,单位为公里。
        express: 是否使用快递,默认为 False。

    Returns:
        运费金额(元)。

    Raises:
        ValueError: 当重量或距离为非正数时。

    Examples:
        >>> calculate_shipping(1.0, 100)
        10.0
        >>> calculate_shipping(1.0, 100, express=True)
        20.0
    """
    if weight <= 0 or distance <= 0:
        raise ValueError("重量和距离必须为正数")
    rate = 0.1 if not express else 0.2
    return weight * distance * rate

25.8 常用工具链

用途工具说明
格式化Ruff一体化 lint + format
类型检查Mypy / Pyright静态类型分析
测试pytest测试框架
文档MkDocs / Sphinx文档生成
包管理uv / Poetry依赖管理
容器Docker应用容器化
CI/CDGitHub Actions自动化流水线
安全bandit, pip-audit安全扫描

25.9 Python 之禅回顾

import this

开发中最重要的原则:

  1. 可读性很重要 — 代码是写给人看的
  2. 显式优于隐式 — 不要隐藏意图
  3. 简单优于复杂 — 选择最简单的方案
  4. 错误不应被静默忽略 — 明确处理异常
  5. 应该有一种明显的方式 — 遵循惯例

25.10 注意事项

🔴 注意

  • 不要过早优化,先让它工作,再让它正确,最后让它快
  • 不要重复造轮子,优先使用标准库和成熟的第三方库
  • 不要忽略类型检查,Mypy 能在运行前发现很多错误
  • 不要跳过测试,自动化测试是代码质量的保障

💡 提示

  • 遵循 PEP 8,使用 Ruff 自动格式化
  • 所有公开接口都写类型注解和文档字符串
  • 每个 PR 都要经过 CI 检查
  • 定期更新依赖,扫描安全漏洞

📌 业务场景

Python 项目启动清单:

□ 选择 Python 版本(3.11+)
□ 创建 pyproject.toml
□ 配置虚拟环境(uv venv)
□ 设置项目结构(src layout)
□ 配置 Ruff + Mypy
□ 编写 .gitignore
□ 设置 GitHub Actions CI
□ 编写 README.md
□ 添加 LICENSE
□ 配置 pre-commit

25.11 扩展阅读


🎉 恭喜完成全部 25 章 Python 编程教程!

持续学习,持续实践。Python 的精髓在于简洁与优雅,用代码表达思想,用测试保障质量,用文档传递知识。