当前位置:首页>python>Python教程 - 单元测试与代码质量

Python教程 - 单元测试与代码质量

  • 2026-09-02 17:17:27
Python教程 - 单元测试与代码质量

欢迎来到"Python教程从零基础到实战"系列的第十六期!

前面我们从环境搭建一路学到了自动化运维,掌握了 Python 的方方面面。但一个重要的问题我们还没有深入讨论:**你怎么确定自己的代码是对的?**

这一期,我们要聊的是软件工程中不可或缺的两个话题:**单元测试**和**代码质量**。学会写测试,你的代码就不再是"能跑就行"的草稿,而是可靠的生产级代码。学会维护代码质量,你的项目才能在大���上保持整洁。

准备好了吗?让我们开始吧!

---

## 一、为什么需要单元测试?

想象一下这个场景:你花了一周时间写了一个数据处理模块,功能完美运行。同事帮你改了两行代码后,你的模块突然报错了——但他不知道自己的改动破坏了你原来的逻辑。

这就是单元测试要解决的问题:**当你(或别人)修改代码时,能立刻知道哪些功能被破坏了。**

单元测试的核心思想很简单:**把程序拆成小块,每一小块单独测试。** 比如一个函数应该接受正确的输入并返回期望的输出。

### 1.1 pytest —— Python 最流行的测试框架

Python 自带 unittest 模块,但社区更推荐用 **pytest**,因为它写起来简单得多。

```bash

pipinstallpytest

```

先看一个基本示例。假设你有一个计算薪水的函数:

```python

# salary_calculator.py

defcalculate_salary(base_salary: float, overtime_hours: float = 0, bonus: float = 0) -> float:

"""计算总薪水:基本工资 + 加班费(每小时1.5倍) + 奖金"""

if base_salary < 0:

raiseValueError("基本工资不能为负数")

    overtime_pay = overtime_hours * (base_salary / 160) * 1.5# 每月标准工时160小时

return base_salary + overtime_pay + bonus

```

对应的测试文件:

```python

# test_salary_calculator.py

import pytest

from salary_calculator import calculate_salary

deftest_basic_salary():

"""测试只有基本工资的情况"""

assert calculate_salary(10000) == 10000

deftest_overtime_pay():

"""测试加班费的计算"""

# 基本工资10000,加班20小时

# 每小时费率 = 10000 / 160 = 62.5

# 加班费 = 20 * 62.5 * 1.5 = 1875

    result = calculate_salary(10000, overtime_hours=20)

assert result == 11875

deftest_with_bonus():

"""测试加上奖金的情况"""

    result = calculate_salary(8000, overtime_hours=10, bonus=2000)

    expected = 8000 + (10 * (8000 / 160) * 1.5) + 2000

assert result == expected

deftest_negative_salary_raises_error():

"""测试负数工资应抛出异常"""

with pytest.raises(ValueError, match="基本工资不能为负数"):

        calculate_salary(-5000)

deftest_zero_overtime():

"""测试零加班的情况"""

assert calculate_salary(5000, overtime_hours=0) == 5000

```

运行测试:

```bash

pytesttest_salary_calculator.py-v

```

输出效果:

```

test_salary_calculator.py::test_basic_salary PASSED

test_salary_calculator.py::test_overtime_pay PASSED

test_salary_calculator.py::test_with_bonus PASSED

test_salary_calculator.py::test_negative_salary_raises_error PASSED

test_salary_calculator.py::test_zero_overtime PASSED

========================= 5 passed in 0.02s =========================

```

### 1.2 测试的参数化 —— 一次写多个测试

如果要对多种输入都测试一遍,手动写很多个函数很麻烦。pytest 提供了 `@pytest.mark.parametrize`:

```python

import pytest

from salary_calculator import calculate_salary

@pytest.mark.parametrize(

"base,overtime,bonus,expected",

    [

        (10000, 0, 0, 10000),

        (5000, 0, 0, 5000),

        (8000, 10, 0, 8000 + 10 * (8000/160) * 1.5),

        (10000, 0, 3000, 13000),

        (0, 0, 0, 0),

    ],

)

deftest_salary_parametrized(base, overtime, bonus, expected):

"""用参数化测试覆盖多种场景"""

assert calculate_salary(base, overtime_hours=overtime, bonus=bonus) == expected

```

一行注解就生成了 5 个测试用例,简洁明了!

---

## 二、Mock —— 测试那些"连接外部"的代码

现实中的代码很少是自给自足的。你的函数可能要读取数据库、调用 API、或者发送电子邮件。测试的时候当然不希望每次运行都真的去查数据库或发邮件。

这时候就需要 **Mock**——模拟(伪装)那些外部依赖。

### 2.1 模拟 API 调用

假设你有一个从天气 API 获取数据的函数:

```python

# weather_service.py

import requests

defget_temperature(city: str) -> float:

"""从天气服务获取指定城市的温度"""

    url = f"https://api.weather.example.com/current?city={city}"

    response = requests.get(url, timeout=10)

    response.raise_for_status()

    data = response.json()

return data["temperature"]

```

直接测试这个函数需要真实的 API 环境,我们用 Mock 来替代:

```python

# test_weather_service.py

import pytest

from unittest.mock import patch, MagicMock

from weather_service import get_temperature

@patch("weather_service.requests.get")

deftest_get_temperature(mock_get):

"""模拟 API 返回"""

# 准备 mock 对象

    mock_response = MagicMock()

    mock_response.json.return_value = {"temperature": 23.5}

    mock_get.return_value = mock_response

# 调用真实函数(内部会走到我们的 mock)

    result = get_temperature("北京")

# 验证

assert result == 23.5

    mock_get.assert_called_once()

    mock_get.assert_called_with(

"https://api.weather.example.com/current?city=%E5%8C%97%E4%BA%AC",

timeout=10

    )

```

`@patch` 装饰器会自动把 `requests.get` 替换为一个 Mock 对象。你的代码以为自己在调 API,实际上拿到的是你准备好的假数据。

### 2.2 模拟数据库查询

再比如一个用户查询函数:

```python

# user_repo.py

classUserRepository:

def__init__(self, db_connection):

self.db = db_connection

defget_user_by_email(self, email: str) -> dict:

"""通过邮箱查找用户"""

        cursor = self.db.cursor()

        cursor.execute("SELECT * FROM users WHERE email = %s", (email,))

        row = cursor.fetchone()

if row:

return {

"id": row[0],

"name": row[1],

"email": row[2],

"created_at": row[3]

            }

returnNone

defcreate_user(self, name: str, email: str) -> int:

"""创建新用户"""

        cursor = self.db.cursor()

        cursor.execute(

"INSERT INTO users (name, email) VALUES (%s, %s)",

            (name, email)

        )

self.db.commit()

return cursor.lastrowid

```

对应的测试:

```python

# test_user_repo.py

import pytest

from unittest.mock import MagicMock

from user_repo import UserRepository

deftest_get_user_by_email_found():

"""测试:用户存在时的查询"""

    mock_db = MagicMock()

    mock_cursor = mock_db.cursor.return_value

    mock_cursor.fetchone.return_value = (1, "张三", "zhangsan@example.com", "2026-01-15")

    repo = UserRepository(mock_db)

    result = repo.get_user_by_email("zhangsan@example.com")

assert result == {

"id": 1,

"name": "张三",

"email": "zhangsan@example.com",

"created_at": "2026-01-15",

    }

    mock_cursor.execute.assert_called_once()

deftest_get_user_by_email_not_found():

"""测试:用户不存在时返回 None"""

    mock_db = MagicMock()

    mock_cursor = mock_db.cursor.return_value

    mock_cursor.fetchone.return_value = None

    repo = UserRepository(mock_db)

    result = repo.get_user_by_email("nobody@example.com")

assert result isNone

deftest_create_user():

"""测试:创建新用户"""

    mock_db = MagicMock()

    mock_cursor = mock_db.cursor.return_value

    mock_cursor.lastrowid = 42

    repo = UserRepository(mock_db)

    user_id = repo.create_user("李四", "lisi@example.com")

assert user_id == 42

    mock_db.commit.assert_called_once()

```

看到没有?**完全没有真实的数据库参与**,但测试覆盖了所有关键路径。这就是 Mock 的力量。

---

## 三、测试覆盖率 —— 你的代码被测试了多少?

光写了测试还不够,你得知道自己**测得够不够**。覆盖率工具会告诉你哪些代码被执行过了,哪些还没被覆盖。

```bash

pipinstallpytest-cov

```

运行带覆盖率的测试:

```bash

pytest--cov=salary_calculator--cov-report=term-missingtest_salary_calculator.py

```

输出类似这样:

```

Name                       Stmts   Miss  Cover   Missing

--------------------------------------------------------

salary_calculator.py           8      0   100%

--------------------------------------------------------

TOTAL                          8      0   100%

```

如果某一行没被覆盖到,"Missing" 栏会告诉你具体是哪一行。

再给你一个实际项目里常见的例子:

```python

# order_manager.py

classOrderManager:

def__init__(self):

self.orders = {}

self.next_id = 1

defcreate_order(self, user_id: str, items: list, total_amount: float) -> int:

"""创建订单"""

ifnot items:

raiseValueError("订单不能为空")

if total_amount <= 0:

raiseValueError("金额必须为正数")

        order_id = self.next_id

self.orders[order_id] = {

"id": order_id,

"user_id": user_id,

"items": items,

"total_amount": total_amount,

"status": "pending"

        }

self.next_id += 1

return order_id

defcancel_order(self, order_id: int) -> bool:

"""取消订单"""

if order_id notinself.orders:

returnFalse

        order = self.orders[order_id]

if order["status"] == "cancelled":

returnFalse

        order["status"] = "cancelled"

returnTrue

defget_order_status(self, order_id: int) -> dict:

"""查询订单状态"""

if order_id notinself.orders:

return {"error": "订单不存在"}

returnself.orders[order_id]

```

测试它:

```python

# test_order_manager.py

import pytest

from order_manager import OrderManager

@pytest.fixture

defmanager():

"""创建一个空的 OrderManager 实例供各测试复用"""

return OrderManager()

deftest_create_order(manager):

"""正常创建订单"""

    order_id = manager.create_order("user_001", [{"name": "键盘", "price": 299}], 299.0)

assert order_id == 1

    status = manager.get_order_status(order_id)

assert status["status"] == "pending"

assert status["total_amount"] == 299.0

deftest_create_second_order(manager):

"""连续创建,ID 递增"""

    id1 = manager.create_order("user_001", [("A", 10)], 10)

    id2 = manager.create_order("user_002", [("B", 20)], 20)

assert id1 == 1

assert id2 == 2

deftest_create_order_empty_items(manager):

"""空列表应抛出异常"""

with pytest.raises(ValueError, match="订单不能为空"):

        manager.create_order("user_001", [], 0)

deftest_create_order_negative_amount(manager):

"""负数金额应抛出异常"""

with pytest.raises(ValueError, match="金额必须为正数"):

        manager.create_order("user_001", [("item", 1)], -5)

deftest_cancel_order(manager):

"""正常取消订单"""

    oid = manager.create_order("user_001", [("A", 10)], 10)

assert manager.cancel_order(oid) isTrue

deftest_cancel_already_cancelled(manager):

"""取消已取消的订单应失败"""

    oid = manager.create_order("user_001", [("A", 10)], 10)

    manager.cancel_order(oid)

assert manager.cancel_order(oid) isFalse

deftest_cancel_nonexistent_order(manager):

"""取消不存在的订单"""

assert manager.cancel_order(999) isFalse

deftest_get_nonexistent_order(manager):

"""查询不存在的订单返回错误"""

    result = manager.get_order_status(999)

assert"error"in result

deftest_create_order_multi_items(manager):

"""创建包含多个商品的订单"""

    items = [

        {"name": "机械键盘", "price": 599},

        {"name": "鼠标垫", "price": 49},

        {"name": "USB线", "price": 15},

    ]

    total = sum(item["price"] for item in items)

    oid = manager.create_order("user_002", items, total)

    status = manager.get_order_status(oid)

assertlen(status["items"]) == 3

assert status["total_amount"] == total

```

运行覆盖率:

```bash

pytest--cov=order_manager--cov-report=term-missingtest_order_manager.py

```

目标:**核心业务代码覆盖率 >= 80%**,关键模块争取 95%+。

### 3.1 覆盖率达标建议

-`pytest.ini` 或 `pyproject.toml` 中可以设置最低覆盖率门槛:

```ini

# pyproject.toml

[tool.pytest.ini_options]

addopts = --cov=. --cov-fail-under=80

```

这样如果覆盖率低于 80%,CI 流程会自动报错,防止低质量代码合入主干。

---

## 四、代码质量工具

写好代码的同时,让代码风格保持一致也非常重要。团队里五个人写代码,五种缩进五种命名,看着就头疼。

下面三个工具是 Python 社区的"标配三剑客":

### 4.1 Black —— 自动格式化

Black 是一个激进的 Python 代码格式化工具。它只有一个目标:**让你的代码格式永远一致,无需争论。**

```bash

pipinstallblack

```

使用方式:

```bash

# 检查格式(不修改文件)

black--checkmy_project/

# 自动格式化所有 Python 文件

blackmy_project/

```

举个例子,不管你怎么写,Black 都会统一成这样:

```python

# 你写的(乱糟糟的)

defhello(name,

age=25,city="Beijing"

 ):print(f"Hi{name},you are {age} from {city}")

# Black 格式化后

defhello(name, age=25, city="Beijing"):

print(f"Hi {name}, you are {age} from {city}")

```

### 4.2 Ruff —— 超快的 Linter

Ruff 是近年来最受欢迎的 Python lint 工具,用 Rust 编写,速度比 flake8 + 一堆插件快 10-100 倍。

```bash

pipinstallruff

```

使用方式:

```bash

# 检查代码质量问题

ruffcheckmy_project/

# 自动修复能安全修复的问题

ruffcheck--fixmy_project/

# 查看详细报告

ruffcheck--output-format=fullmy_project/

```

Ruff 能发现很多问题:

```python

# utils.py

import os, sys  # Ruff: 多个 import 应分开

unused_var = "hello"# Ruff: 定义了但没使用的变量

defcalc(x):

return x + 1

print("done")  # Ruff: 永远不会执行的代码(dead code)

```

运行 `ruff check` 会给出:

```

utils.py:1:8: F401 [*] `os` and `sys` imported but unused

utils.py:3:1: F841 local variable `unused_var` is assigned to but never used

utils.py:6:5: F401 [*] `os` and `sys` imported but unused

utils.py:7:5: F601 assignment to `x` after potential reference to `x`

```

### 4.3 mypy —— 静态类型检查

如果你的代码中有类型注解,mypy 可以在不运行代码的情况下发现类型错误:

```bash

pipinstallmypy

```

```python

# calculator.py

defdivide(a: float, b: float) -> float:

return a / b

defmultiply(a: int, b: int) -> int:

return a * b

# 类型检查示例

result1 = divide(10.0, 3.0)      # OK

result2 = multiply("5", "3")      # mypy 会报错:参数类型错误

result3: str = divide(10.0, 3.0)  # mypy 会报错:返回的是 float 却赋给了 str

```

运行 `mypy calculator.py`:

```

calculator.py:9: error: Argument 1 to "multiply" has incompatible type "str"; expected "int"

calculator.py:10: error: Incompatible types in assignment (expression has type "float", variable has type "str")

```

有了这些工具,你可以把它们集成到 IDE 或 CI 流程中,让代码质量问题在提交前就被自动发现。

---

## 五、端到端实战:一个完整的测试驱动开发示例

理论学得差不多了,现在来一个综合练习。我们用**测试驱动开发(TDD)**的方式实现一个小项目:**一个简单的备忘录应用**。

先写测试,再写代码。

### Step 1:定义测试

```python

# test_memo.py

import pytest

from memo import MemoApp, MemoItem

deftest_create_memo():

"""创建备忘录"""

    app = MemoApp()

    item_id = app.create("记住购买 supplies", "urgent")

    items = app.list_all()

assertlen(items) == 1

assert items[0].content == "记住购买 supplies"

assert items[0].priority == "urgent"

assert items[0].done isFalse

deftest_complete_memo():

"""标记已完成"""

    app = MemoApp()

    item_id = app.create("学会 pytest", "normal")

    app.complete(item_id)

    item = app.get(item_id)

assert item.done isTrue

deftest_delete_memo():

"""删除备忘录"""

    app = MemoApp()

    item_id = app.create("删除我", "low")

    app.delete(item_id)

assertlen(app.list_all()) == 0

deftest_filter_by_priority():

"""按优先级筛选"""

    app = MemoApp()

    app.create("重要事项", "urgent")

    app.create("随便想想", "low")

    app.create("中等重要", "normal")

    urgent_items = app.filter_by_priority("urgent")

assertlen(urgent_items) == 1

assert urgent_items[0].content == "重要事项"

deftest_stats():

"""统计信息"""

    app = MemoApp()

    app.create("任务一", "urgent")

    app.create("任务二", "normal")

    app.create("任务三", "urgent")

    stats = app.stats()

assert stats["total"] == 3

assert stats["done"] == 0

assert stats["undone"] == 3

assert stats["by_priority"]["urgent"] == 2

assert stats["by_priority"]["normal"] == 1

```

### Step 2:实现代码

```python

# memo.py

from dataclasses import dataclass, field

from typing import Dict, List, Optional

@dataclass

classMemoItem:

"""备忘录条目"""

id: int

    content: str

    priority: str

    done: bool = False

classMemoApp:

"""简单的备忘录应用"""

def__init__(self):

self._items: Dict[int, MemoItem] = {}

self._next_id: int = 1

defcreate(self, content: str, priority: str = "normal") -> int:

"""创建备忘录条目"""

        item = MemoItem(

id=self._next_id,

content=content,

priority=priority

        )

self._items[self._next_id] = item

self._next_id += 1

return item.id

defcomplete(self, item_id: int) -> bool:

"""标记备忘录为已完成"""

if item_id notinself._items:

returnFalse

self._items[item_id].done = True

returnTrue

defdelete(self, item_id: int) -> bool:

"""删除备忘录"""

if item_id inself._items:

delself._items[item_id]

returnTrue

returnFalse

defget(self, item_id: int) -> Optional[MemoItem]:

"""获取指定备忘录"""

returnself._items.get(item_id)

deflist_all(self) -> List[MemoItem]:

"""列出所有未删除的备忘录"""

returnlist(self._items.values())

deffilter_by_priority(self, priority: str) -> List[MemoItem]:

"""按优先级筛选"""

return [item for item inself._items.values() if item.priority == priority]

defstats(self) -> dict:

"""返回统计信息"""

        all_items = self.list_all()

        done_items = [i for i in all_items if i.done]

        undone_items = [i for i in all_items ifnot i.done]

        by_priority = {}

for item in all_items:

            by_priority[item.priority] = by_priority.get(item.priority, 0) + 1

return {

"total": len(all_items),

"done": len(done_items),

"undone": len(undone_items),

"by_priority": by_priority,

        }

```

### Step 3:运行测试

```bash

pytesttest_memo.py-v

```

```

test_memo.py::test_create_memo PASSED

test_memo.py::test_complete_memo PASSED

test_memo.py::test_delete_memo PASSED

test_memo.py::test_filter_by_priority PASSED

test_memo.py::test_stats PASSED

========================= 5 passed in 0.01s =========================

```

全部通过!🎉 这就是 TDD 的基本流程:**红-绿-重构**。

-**红**:先写失败的测试

-**绿**:写出让测试通过的代码

-**重构**:优化代码结构,重新运行测试确认没破坏任何东西

---

## 六、练习题

### 练习 1:测试数学工具函数

写一个 `math_utils.py` 包含求阶乘和判断素数的函数,再写对应的测试。

```python

# math_utils.py

deffactorial(n: int) -> int:

"""计算 n 的阶乘"""

if n < 0:

raiseValueError("不允许负数")

if n in (0, 1):

return1

    result = 1

for i inrange(2, n + 1):

        result *= i

return result

defis_prime(n: int) -> bool:

"""判断是否为素数"""

if n < 2:

returnFalse

for i inrange(2, int(n ** 0.5) + 1):

if n % i == 0:

returnFalse

returnTrue

```

要求编写测试覆盖边界情况(0、1、负数、小数输入等)。

### 练习 2:用 pytest fixture 管理测试数据

```python

# 提示:使用 @pytest.fixture 创建共享的测试数据

import pytest

@pytest.fixture

defsample_users():

return [

        {"id": 1, "name": "Alice", "age": 30},

        {"id": 2, "name": "Bob", "age": 25},

        {"id": 3, "name": "Charlie", "age": 35},

    ]

deftest_find_adults(sample_users):

"""找出年龄 >= 30 的用户"""

    adults = [u for u in sample_users if u["age"] >= 30]

assertlen(adults) == 2

assert adults[0]["name"] == "Alice"

```

尝试扩展这个 fixture 并写更多测试。

### 练习 3:给之前的某个函数写完整测试

从 Episode 05 的爬虫代码中选一个函数,给它写全覆盖的测试(包括 Mock 网络请求的部分)。

### 练习 4:运行 ruff 和 black

在你的任意 Python 项目中安装 Black 和 Ruff,运行格式化检查和代码质量扫描,然后修复所有问题。

### 练习 5:实现带缓存的斐波那契数列(含测试)

```python

deffibonacci(n: int, _cache: dict = None) -> int:

"""带缓存优化的斐波那契"""

if _cache isNone:

        _cache = {}

if n in _cache:

return _cache[n]

if n <= 0:

raiseValueError("n 必须是正整数")

if n == 1or n == 2:

return1

    result = fibonacci(n - 1, _cache) + fibonacci(n - 2, _cache)

    _cache[n] = result

return result

```

为这个函数编写测试,验证前 10 项的结果。

---

## 七、参考答案

### 练习 1 参考

```python

# test_math_utils.py

import pytest

from math_utils import factorial, is_prime

deftest_factorial_zero():

assert factorial(0) == 1

deftest_factorial_one():

assert factorial(1) == 1

deftest_factorial_five():

assert factorial(5) == 120

deftest_factorial_large():

assert factorial(10) == 3628800

deftest_factorial_negative():

with pytest.raises(ValueError, match="不允许负数"):

        factorial(-1)

@pytest.mark.parametrize("n,expected", [

    (2, True),

    (3, True),

    (4, False),

    (17, True),

    (1, False),

    (0, False),

    (-5, False),

    (97, True),

])

deftest_is_prime(n, expected):

assert is_prime(n) == expected

```

### 练习 5 参考

```python

# test_fibonacci.py

import pytest

from math_utils_exercise import fibonacci

@pytest.mark.parametrize("n,expected", [

    (1, 1),

    (2, 1),

    (3, 2),

    (4, 3),

    (5, 5),

    (6, 8),

    (7, 13),

    (8, 21),

    (9, 34),

    (10, 55),

])

deftest_fibonacci_values(n, expected):

assert fibonacci(n) == expected

deftest_fibonacci_performance():

"""缓存优化应该让大数计算飞快"""

import time

    start = time.time()

    fibonacci(1000)

    elapsed = time.time() - start

# 1000 项应该在 1ms 内算完

assert elapsed < 0.01

deftest_fibonacci_invalid():

with pytest.raises(ValueError):

        fibonacci(0)

with pytest.raises(ValueError):

        fibonacci(-10)

```

---

## 八、总结

这一期我们学了很多实用的工程技能:

-**pytest 基础**:`assert`、`@pytest.fixture`、`@pytest.mark.parametrize`、异常测试

-**Mock 技术**:用 `unittest.mock` 模拟外部依赖(API、数据库)

-**测试覆盖率**:用 `pytest-cov` 量化测试质量

-**代码质量工具**:Black 格式化、Ruff 检查、mypy 类型验证

-**TDD 实践**:测试驱动开发的完整流程

这些工具不是"可选的加分项",而是专业开发的标配。当你养成写测试的习惯,你会发现:

1.**改代码不再忐忑了**——测试替你盯着

2.**bug 变少了**——因为你会想"我怎么测试这个"才去写代码

3.**文档更清晰了**——测试就是最准确的文档

---

## 下期预告

📢 **第十七期:Python 设计模式实战——让代码更优雅的艺术**

我们已经学了如何写能用的代码、如何保证代码正确。接下来,我们要学如何让代码写得**更好**。

第十七期将介绍最常用的几种设计模式:

-**单例模式**:确保一个类只有一个实例

-**工厂模式**:隐藏创建逻辑,统一接口

-**装饰器模式**:动态给对象添加功能(没错,Episode 4 的装饰器升级版)

-**观察者模式**:事件驱动架构的基础

-**策略模式**:算法可以随时切换

- 配合 pytest 测试每种模式的实现

敬请期待!

---

**📚 系列回顾**

到目前为止我们学过的内容:

| Episode | 主题 |

|---------|------|

| 01 | 环境搭建与基础语法 |

| 02 | 数据结构与字符串处理 |

| 03 | 面向对象编程 |

| 04 | 装饰器、生成器与文件 IO |

| 05 | 爬虫入门 |

| 06 | 数据分析入门 |

| 07 | Web 后端开发 |

| 08 | AI 实战入门 |

| 09 | 前端入门 |

| 10 | 项目实战——Dashboard |

| 11 | Docker 容器化部署 |

| 12 | 数据库进阶 |

| 13 | 异步编程与并发 |

| 14 | 日志系统与调试技巧 |

| 15 | 自动化运维与脚本实战 |

| 16 | 单元测试与代码质量 ← **本期** |

继续加油,你已经在成为 Python 全栈工程师的路上了!🚀

最新文章

随机文章