1. 程序员与工程师的区别

兄弟们,如果你只会写代码,那你只是个"码农"。如果你能保证你的代码在一年后还能跑、能被别人看懂、能稳定上线,那你才是"工程师"。

很多初学者觉得:

  • “写测试太浪费时间,我手动点点不就行了?”
  • “Git 太麻烦,我直接压缩包发给同事不行吗?”
  • “上线不就是把代码拷贝到服务器上吗?”

这一讲,我们要聊聊**“工程化”**。它是把你的代码从"草台班子"变成"正规军"的关键。

工程化思维的核心在于:可维护性、可追溯性、可重复性。这三个特性决定了你的项目能否在团队协作和长期迭代中存活下来。


2. 自动化测试:你的"免死金牌"

想象一下,你改了一个小 Bug,结果导致原本正常的支付功能崩了。如果你有自动化测试,这事儿根本不会发生。

2.1 为什么用 pytest

它是 Python 界最流行的测试框架,写起来最简单,功能也最强。相比 Python 自带的 unittestpytest 的优势在于:

  • 不需要写类,直接用函数就能测试
  • 断言用原生的 assert,简单直观
  • 插件生态丰富,支持参数化、覆盖率报告等
# calculator.py
def add(a, b):
    return a + b

# test_calculator.py
import pytest
from calculator import add

def test_add_success():
    assert add(1, 2) == 3

def test_add_negative():
    assert add(-1, 1) == 0

运行命令:在终端输入 pytest,它会自动找到所有 test_ 开头的文件并运行。全绿,你就能安心下班;有红,赶紧修。

2.2 测试驱动开发(TDD)理念

TDD(Test-Driven Development,测试驱动开发)是一种"先写测试,再写代码"的开发模式。听起来有点反直觉,但它是保证代码质量的金标准。

TDD 的核心循环:红-绿-重构

1. 红:写一个失败的测试(因为功能还没实现)
2. 绿:写最少的代码让测试通过
3. 重构:优化代码,保持测试通过

TDD 的优势

  • 需求明确:写测试前必须先想清楚输入输出,避免盲目编码
  • 代码可测试:被迫写出松耦合、可测试的代码
  • 回归保障:每次修改都有测试守护,不怕改坏旧功能
  • 文档作用:测试代码就是最好的使用文档

TDD 实战示例:实现一个用户注册功能

# test_user.py - 第一步:先写测试(红)
import pytest
from user import UserService

def test_register_user_success():
    service = UserService()
    user = service.register("alice", "password123")
    assert user.username == "alice"
    assert user.id is not None

def test_register_duplicate_username():
    service = UserService()
    service.register("bob", "password123")
    with pytest.raises(ValueError, match="用户名已存在"):
        service.register("bob", "another_password")

# user.py - 第二步:实现代码让测试通过(绿)
import uuid

class User:
    def __init__(self, username, user_id):
        self.username = username
        self.id = user_id

class UserService:
    def __init__(self):
        self._users = {}
  
    def register(self, username, password):
        if username in self._users:
            raise ValueError("用户名已存在")
        user = User(username, str(uuid.uuid4()))
        self._users[username] = user
        return user

一句话总结:TDD 不是"多写代码",而是"把思考前置",让 Bug 在出生前就被消灭。

2.3 Pytest 断言机制详解

断言是测试的核心灵魂。pytest 的断言比 unittest 简洁太多,直接用 Python 原生的 assert 语句即可。

常用断言类型

断言方式 说明 示例
assert a == b 判断相等 assert add(1,2) == 3
assert a != b 判断不等 assert result != 0
assert a in b 判断包含 assert "error" in message
assert a is None 判断为空 assert user is None
assert not a 判断为假 assert not is_valid

异常断言:测试代码是否按预期抛出异常。

import pytest
from calculator import divide

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

参数化测试:同一个测试逻辑,用多组数据跑。

@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (-1, 1, 0),
    (0, 0, 0),
    (100, -50, 50),
])
def test_add_parametrize(a, b, expected):
    assert add(a, b) == expected

这样写一次测试函数,就能跑四组数据,大大减少重复代码。

2.4 代码覆盖率分析

写了测试,但怎么知道测试得够不够全面?这时候需要**代码覆盖率(Code Coverage)**工具。

什么是覆盖率

  • 行覆盖率:有多少比例的代码行被执行过
  • 分支覆盖率:有多少比例的条件分支(if/else)被执行过
  • 函数覆盖率:有多少比例的函数被调用过

安装和使用 coverage

# 安装 pytest-cov 插件
pip install pytest-cov

# 运行测试并生成覆盖率报告
pytest --cov=calculator --cov-report=term-missing

# 生成 HTML 报告(更直观)
pytest --cov=calculator --cov-report=html

覆盖率报告示例

Name            Stmts   Miss  Cover   Missing
---------------------------------------------
calculator.py      15      3    80%   12, 15-16
test_calc.py       20      0   100%
---------------------------------------------
TOTAL              35      3    91%

解读

  • Stmts:语句总数
  • Miss:未执行的语句数
  • Cover:覆盖率百分比
  • Missing:未执行的行号

注意:100% 覆盖率不等于没有 Bug!覆盖率只是告诉你哪些代码没跑到,不能保证逻辑正确。关键是测试边界条件异常情况

提升覆盖率的技巧

  1. 测试正常路径和异常路径
  2. 测试边界值(如空列表、最大值、最小值)
  3. 使用参数化测试覆盖多种输入组合
  4. 对复杂逻辑进行分支覆盖测试

3. Git:程序员的"时光机"

如果你没用过 Git,你肯定干过这种事:代码_最终版.py, 代码_最终版2.py, 代码_打死不改版.py……

3.1 常用命令四部曲

  1. git init:把这个文件夹管起来。
  2. git add .:把改动存进"暂存区"。
  3. git commit -m "重构了登录逻辑":拍个快照,记下你干了啥。
  4. git push:把代码推送到 GitHub 或 GitLab,再也不怕电脑坏了。

3.2 Git 工作流详解

在团队协作中,分支管理是一门大学问。最常见的策略叫 Git Flow,它定义了五种分支:

分支类型 名称 用途 生命周期
主分支 main / master 生产环境代码,永远稳定 永久
开发分支 develop 开发集成分支,日常开发的基础 永久
功能分支 feature/* 开发新功能 临时,完成后合并到 develop
发布分支 release/* 准备发布版本 临时,完成后合并到 main
修复分支 hotfix/* 紧急修复线上 Bug 临时,完成后合并到 main 和 develop

典型工作流

# 1. 从 develop 创建功能分支
git checkout develop
git checkout -b feature/user-login

# 2. 开发完成后合并回 develop
git checkout develop
git merge feature/user-login

# 3. 准备发布时创建 release 分支
git checkout -b release/v1.0.0

# 4. 发布到 main
git checkout main
git merge release/v1.0.0
git tag -a v1.0.0 -m "Release version 1.0.0"

一句话总结main 是稳的,develop 是新的,feature 是你干活的地方,hotfix 是救火的。

3.3 Git 工作流深度解析

除了 Git Flow,还有几种常用的工作流模式,适合不同规模的团队:

1. GitHub Flow(轻量级)

适合持续部署的团队,流程极简:

main 分支永远可部署
  |
  +-- 创建 feature 分支
  |
  +-- 提交 Pull Request(PR)
  |
  +-- Code Review 通过
  |
  +-- 合并到 main,自动部署

2. GitLab Flow(带环境分支)

适合需要多环境部署的场景:

feature 分支 -> develop 分支 -> staging 分支 -> main 分支
     (开发)        (集成测试)       (预发布)        (生产)

3. Trunk-Based Development(主干开发)

适合高频发布、自动化程度高的团队:

  • 所有开发都在 main 分支进行
  • 功能用"特性开关"(Feature Toggle)控制
  • 每天多次合并到主干

Commit 规范(Conventional Commits)

规范的提交信息方便生成 CHANGELOG 和自动化版本管理:

<type>(<scope>): <subject>

<body>

<footer>

常用 type

类型 说明
feat 新功能
fix 修复 Bug
docs 文档更新
style 代码格式(不影响功能)
refactor 重构
test 测试相关
chore 构建/工具相关

示例

git commit -m "feat(auth): 添加 JWT Token 验证功能

- 实现登录接口返回 Token
- 添加 Token 过期校验中间件
- 更新 API 文档

Closes #123"

4. 别再用 print 调试了:Logging 才是王道

在生产环境里,你没法盯着屏幕看。所有的错误、运行状态都得写进日志文件。

4.1 日志级别详解

Python 的 logging 模块定义了五个等级,从低到高:

等级 数值 使用场景
DEBUG 10 调试信息,开发时用,生产环境一般关闭
INFO 20 正常运行信息,如"订单创建成功"
WARNING 30 警告信息,不影响运行但需要关注
ERROR 40 错误信息,功能出问题了但程序没崩
CRITICAL 50 严重错误,程序可能要挂了

等级的作用:设置一个阈值,低于这个等级的日志不会输出。比如设置 level=logging.INFO,那 DEBUG 级别的日志就不会显示。

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    filename='app.log'
)

def process_order(order_id):
    logging.info(f"正在处理订单:{order_id}")
    try:
        pass
    except Exception as e:
        logging.error(f"订单 {order_id} 处理失败:{e}")

process_order("ORD-123")

4.2 日志格式常用占位符

占位符 含义
%(asctime)s 时间戳
%(name)s Logger 名称
%(levelname)s 日志等级
%(message)s 日志内容
%(filename)s 文件名
%(lineno)d 行号

4.3 日志轮转策略

生产环境的日志文件会不断增长,最终占满磁盘。日志轮转(Log Rotation)可以自动切割和清理旧日志。

两种常用轮转方式

方式 触发条件 适用场景
按大小轮转 RotatingFileHandler 文件大小超过阈值 高频日志
按时间轮转 TimedRotatingFileHandler 时间到达(每天/每小时) 低频但重要的日志

按大小轮转示例

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("MyApp")
logger.setLevel(logging.INFO)

# 单个文件最大 10MB,保留 5 个备份
handler = RotatingFileHandler(
    "app.log", 
    maxBytes=10*1024*1024,  # 10MB
    backupCount=5,
    encoding="utf-8"
)

formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)

# 使用
logger.info("用户登录成功")
logger.error("数据库连接失败", exc_info=True)  # 记录异常堆栈

按时间轮转示例

from logging.handlers import TimedRotatingFileHandler

# 每天午夜轮转,保留 30 天
handler = TimedRotatingFileHandler(
    "app.log",
    when="midnight",  # 可选:S(秒), M(分), H(时), D(天), W(周)
    interval=1,
    backupCount=30,
    encoding="utf-8"
)

日志级别与环境配置建议

环境 日志级别 输出目标 备注
开发环境 DEBUG 控制台 + 文件 详细日志方便调试
测试环境 INFO 文件 关注业务逻辑
生产环境 WARNING 文件 + 监控系统 减少噪音,关注异常

5. 部署:让你的程序在云端跑起来

5.1 requirements.txt:你的"环境清单"

当你把代码发给别人时,他怎么知道要装哪些库?

# 生成清单
pip freeze > requirements.txt

# 别人拿到后一键安装
pip install -r requirements.txt

5.2 Docker:标准化的"集装箱"

现在最流行的部署方式是 Docker。它能保证"在我的电脑上能跑,在服务器上也能跑"。

5.3 Docker 容器化原理

什么是容器
容器是"轻量级的虚拟机",但它不是虚拟硬件,而是共享宿主机的内核,只隔离进程、文件系统和网络。这使得容器启动快(秒级)、资源占用少。

容器 vs 虚拟机

特性 容器 (Docker) 虚拟机 (VM)
启动速度 秒级 分钟级
资源占用 少(共享内核) 多(需要完整 OS)
隔离性 进程级 硬件级
体积 小(MB 级) 大(GB 级)
性能 接近原生 有虚拟化开销

Docker 核心概念

Dockerfile(配方)
    |
    v
docker build(构建)
    |
    v
Image(镜像,只读模板)
    |
    v
docker run(运行)
    |
    v
Container(容器,运行实例)

Dockerfile 指令详解

指令 作用 示例
FROM 指定基础镜像 FROM python:3.12-slim
WORKDIR 设置工作目录 WORKDIR /app
COPY 复制文件到镜像 COPY . .
RUN 构建时执行的命令 RUN pip install -r requirements.txt
CMD 容器启动时执行的命令 CMD ["python", "main.py"]
EXPOSE 声明暴露的端口 EXPOSE 8000
ENV 设置环境变量 ENV DEBUG=False

镜像分层原理
Docker 镜像由多层组成,每层是前一层的增量。如果多个镜像共享同一层,磁盘上只存一份。这就是为什么 Dockerfile 中要把不常变动的指令(如 FROMRUN pip install)放在前面,常变动的(如 COPY . .)放在后面,可以充分利用缓存加速构建。

一个完整的 Dockerfile 示例:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

ENV DEBUG=False

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

构建和运行命令

# 构建镜像
docker build -t my-python-app .

# 运行容器
docker run -d -p 8000:8000 my-python-app

# 查看运行中的容器
docker ps

# 查看容器日志
docker logs <container_id>

# 进入容器内部调试
docker exec -it <container_id> /bin/bash

Docker Compose:多容器编排

实际项目往往需要多个容器(如 Web 服务 + 数据库 + Redis):

# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DB_HOST=db
      - REDIS_HOST=redis
    depends_on:
      - db
      - redis
  
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
  
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:
# 一键启动所有服务
docker-compose up -d

# 停止所有服务
docker-compose down

6. 避坑小贴士(老司机的叮嘱)

  1. 别把敏感信息传到 Git:数据库密码、API 密钥,绝对不能写在代码里传到 GitHub。用 .env 文件配合 python-dotenv 库。
  2. Commit 信息要有意义:别老是写 update, fix。写清楚 fix: 修复了订单支付重复扣款的 Bug,以后找起来想哭的心都有。
  3. 测试覆盖率不等于质量:100% 的测试覆盖率不代表没 Bug。关键是要测试那些"极端情况"和"边界条件"。
  4. 日志不是越多越好:生产环境不要开 DEBUG 级别,日志文件会爆炸。定期清理或使用日志轮转。
  5. Docker 镜像要精简:用 slimalpine 版本的基础镜像,能大幅减小镜像体积。

7. 实战演练:巩固你的内功

题目 1:Pytest Fixture 实战

需求
使用 pytestfixture 功能。

  1. 定义一个 fixture db_conn,模拟数据库连接(打印 “连接DB”),测试结束后关闭(打印 “关闭DB”)。
  2. 编写一个测试函数 test_query,使用该 fixture,并断言查询结果。
点击查看参考答案
import pytest

@pytest.fixture
def db_conn():
    print("\n[Setup] 连接数据库...")
    conn = {"status": "connected", "data": [1, 2, 3]}
    yield conn
    print("\n[Teardown] 关闭数据库...")

def test_query(db_conn):
    assert db_conn["status"] == "connected"
    assert len(db_conn["data"]) == 3
    print("查询测试通过!")

题目 2:日志轮转 (Rotating Logs)

需求
配置 logging,使其将日志写入 app.log
当日志文件超过 1KB 时,自动切割备份,最多保留 3 个备份文件。
使用循环写入足够多的日志来触发轮转。

点击查看参考答案
import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("MyLogger")
logger.setLevel(logging.INFO)

handler = RotatingFileHandler("app.log", maxBytes=1024, backupCount=3, encoding="utf-8")
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

logger.addHandler(handler)

for i in range(100):
    logger.info(f"这是一条测试日志,编号:{i},用来测试日志轮转功能是否正常工作。")
  
print("日志写入完成,请查看目录下的 app.log 及其备份文件。")

题目 3:环境变量管理 (.env)

需求
模拟从 .env 文件加载配置。

  1. 创建 .env 文件(模拟写入)。
  2. 使用 python-dotenv 加载。
  3. 读取 DB_HOSTDB_PORT,如果不存在则使用默认值。
点击查看参考答案
import os
from dotenv import load_dotenv

with open(".env", "w") as f:
    f.write("DB_HOST=localhost\nDB_PORT=3306\nSECRET_KEY=123456")

load_dotenv(override=True)

db_host = os.getenv("DB_HOST", "127.0.0.1")
db_port = int(os.getenv("DB_PORT", 8000))
secret = os.getenv("SECRET_KEY")

print(f"数据库地址: {db_host}:{db_port}")
print(f"密钥: {secret}")

os.remove(".env")

8. 系列索引


写在最后
这一讲学完,你已经从一个"写代码的"进化成了"做工程的"。工程化思维会让你在职业生涯中走得更远。
别只是看,去 GitHub 注册个账号,把你写的学生管理系统或者 To-Do API 传上去。
觉得有收获的话,点赞、收藏!咱们下一讲,也是最后一讲,聊聊网络协议和 API 设计,顺便给这个系列做个圆满的总结!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐