Python单元测试实战:从pytest入门到Mock高级技巧
1. 项目概述为什么单元测试是Python开发的“安全带”写Python代码尤其是业务逻辑稍微复杂一点的项目最怕什么不是语法错误那种IDE会直接标红。最怕的是那种“静默的崩溃”——代码能跑输出看起来也对但某个边界条件没处理好或者某次代码改动无意中影响了另一个看似无关的功能。这种问题往往在测试环境发现不了一上线就出事儿。我干了十多年开发踩过无数次这种坑后来才明白单元测试就是给代码系上的那根“安全带”它不能保证你不撞车但能在你犯错时最大程度地减少伤害。简单说单元测试就是针对程序中最小的可测试单元在Python里通常是一个函数或一个类的方法进行正确性检验的验证工作。它的核心思想是隔离与验证把要测试的代码从复杂的依赖环境中剥离出来单独验证其输入输出是否符合预期。很多人尤其是初学者觉得写测试代码是浪费时间不如直接运行看结果。但当你维护一个超过三个月、有上万行代码的项目时没有单元测试你几乎不敢做任何修改因为完全不知道会“炸”到哪里。从你提供的热搜词就能看出大家的痛点vue单元测试报错、flink的单元测试介绍及示例、java单元测试skill、c#单元测试、代码覆盖率、前端使用 单元测试怎么做……这恰恰说明无论前后端、大数据还是其他语言单元测试都是绕不开的硬核技能。Python作为一门以简洁高效著称的语言其内置的unittest框架和第三方强大的pytest让编写测试变得相对轻松。但“会写”和“写好”之间隔着巨大的经验鸿沟。这篇文章我就结合自己从写“脆弱测试”到“稳固测试”的实战经验用一个完整的实例带你拆解Python单元测试的每一个核心环节让你写的测试代码真正成为项目的守护神而不是累赘。2. 核心工具选型unittest 还是 pytest工欲善其事必先利其器。Python社区主流的单元测试框架有两个标准库自带的unittest和第三方框架pytest。很多新手会纠结选哪个我的建议是新手从unittest入门理解概念实际项目强烈推荐pytest。2.1 unittest标准库的“教科书”unittest是Python标准库的一部分这意味着你不需要额外安装任何包。它的设计灵感来源于Java的JUnit采用了面向对象的方式组织测试。它的核心概念包括TestCase测试用例所有测试用例的基类。你写的每一个测试类都需要继承unittest.TestCase。test method测试方法以test_开头的方法会被自动识别为测试方法。Fixture固件setUp()和tearDown()方法用于在每个测试方法执行前后进行准备和清理工作。Assert断言一系列assertXxx方法如assertEqual(a, b)用于验证结果。unittest的优势在于“正统”和“内置”。它的结构非常清晰强制你按照类和方法来组织测试适合教学和理解单元测试的基本范式。但它的缺点也很明显写法相对繁琐必须继承TestCase断言方法不够直观assertEqualvs 直接的assert a b并且缺乏很多现代测试框架的便利特性。2.2 pytest现代项目的“瑞士军刀”pytest是一个第三方框架需要通过pip install pytest安装。它几乎成为了Python社区单元测试的事实标准。pytest的核心理念是“约定优于配置”和“极简主义”无需继承测试函数以test_开头测试类以Test开头其中的方法以test_开头就会被自动发现。断言即普通assert直接使用Python原生的assert语句失败时会给出极其详细的差异分析这对调试是巨大的福音。丰富的Fixture系统功能远超setUp/tearDown。你可以通过pytest.fixture装饰器定义可重用的测试资源并通过函数参数注入的方式在测试中使用管理生命周期函数级、类级、模块级、会话级非常灵活。插件生态强大有海量插件支持并行测试(pytest-xdist)、生成HTML报告(pytest-html)、控制用例执行顺序等。我的选择与理由在绝大多数生产项目中我毫无悬念地选择pytest。它的语法更Pythonic写起来更简洁调试信息更友好Fixture系统能优雅地解决复杂的测试依赖和资源管理问题。对于新手我建议先花一点时间了解unittest的基本概念然后迅速切换到pytest进行实战。本文后续的实例也将基于pytest展开因为它代表了更高效、更现代的测试实践。3. 实战构建一个可测试的“用户钱包”模块光讲理论太枯燥我们用一个贴近业务的例子来贯穿始终一个简单的“用户钱包”模块。这个模块负责处理用户的余额、充值、消费等操作。我们会先写出业务代码然后为其编写完整的单元测试。3.1 定义业务逻辑与潜在缺陷首先我们创建业务代码文件wallet.py# wallet.py class InsufficientBalanceError(Exception): 自定义异常余额不足 pass class Wallet: def __init__(self, owner: str, initial_balance: float 0.0): if initial_balance 0: raise ValueError(初始余额不能为负数) self.owner owner self._balance initial_balance self._transaction_history [] property def balance(self) - float: return self._balance def deposit(self, amount: float) - float: 充值 if amount 0: raise ValueError(充值金额必须大于0) self._balance amount self._record_transaction(deposit, amount) return self._balance def withdraw(self, amount: float) - float: 取款/消费 if amount 0: raise ValueError(取款金额必须大于0) if amount self._balance: raise InsufficientBalanceError(f余额不足。当前余额{self._balance} 尝试取款{amount}) self._balance - amount self._record_transaction(withdraw, amount) return self._balance def transfer(self, target_wallet: Wallet, amount: float) - None: 转账给另一个钱包 if not isinstance(target_wallet, Wallet): raise TypeError(目标必须是一个Wallet实例) # 先从本钱包扣款 self.withdraw(amount) # 再向目标钱包存款 target_wallet.deposit(amount) self._record_transaction(transfer_out, amount, target_wallet.owner) target_wallet._record_transaction(transfer_in, amount, self.owner) def _record_transaction(self, tx_type: str, amount: float, counterparty: str ): 内部方法记录交易历史 import datetime record { timestamp: datetime.datetime.now().isoformat(), type: tx_type, amount: amount, balance_after: self._balance, counterparty: counterparty } self._transaction_history.append(record) def get_recent_transactions(self, n: int 5): 获取最近n条交易记录 if n 0: return [] return self._transaction_history[-n:]看看这段代码里面埋了多少需要测试的“雷”初始余额不能为负、充值和取款金额必须为正、取款时余额要足够、转账的目标对象必须正确、交易记录要准确……如果不写测试这些逻辑分支很可能在某个边缘情况下出错。3.2 搭建测试脚手架与编写第一个测试接下来我们创建测试文件test_wallet.py。按照pytest的约定以test_开头的文件会被自动发现。# test_wallet.py import pytest from wallet import Wallet, InsufficientBalanceError # 测试类名以Test开头 class TestWallet: 测试Wallet类的基本功能 # 这是一个pytest fixture它会在每个测试方法前执行返回一个新的Wallet实例 pytest.fixture def fresh_wallet(self): 提供一个初始余额为100的新钱包 return Wallet(TestUser, 100.0) # 测试方法以test_开头 def test_initialization(self): 测试钱包初始化 # 正常初始化 w Wallet(Alice, 50.0) assert w.owner Alice assert w.balance 50.0 # 测试默认初始余额 w2 Wallet(Bob) assert w2.balance 0.0 # 测试异常情况初始余额为负应该抛出ValueError with pytest.raises(ValueError, match初始余额不能为负数): Wallet(Charlie, -10.0) def test_deposit(self, fresh_wallet): 测试充值功能 w fresh_wallet initial_balance w.balance # 正常充值 new_balance w.deposit(50.0) assert new_balance initial_balance 50.0 assert w.balance new_balance # 验证属性与返回值一致 # 充值金额为0或负数应抛出异常 with pytest.raises(ValueError, match充值金额必须大于0): w.deposit(0) with pytest.raises(ValueError): w.deposit(-5.0) def test_withdraw_normal(self, fresh_wallet): 测试正常取款 w fresh_wallet new_balance w.withdraw(30.0) assert new_balance 70.0 assert w.balance 70.0 def test_withdraw_insufficient_balance(self, fresh_wallet): 测试余额不足取款 w fresh_wallet with pytest.raises(InsufficientBalanceError): w.withdraw(200.0) # 尝试取款超过余额 def test_withdraw_invalid_amount(self, fresh_wallet): 测试取款金额非法 w fresh_wallet with pytest.raises(ValueError): w.withdraw(-10.0) with pytest.raises(ValueError): w.withdraw(0) # 参数化测试用一组数据测试同一个逻辑 pytest.mark.parametrize(initial, deposit_amt, expected, [ (0, 50, 50), (100, 25.5, 125.5), (10, 0.01, 10.01), ]) def test_deposit_parametrized(self, initial, deposit_amt, expected): 使用参数化测试多组充值数据 w Wallet(ParamUser, initial) w.deposit(deposit_amt) # 使用pytest.approx处理浮点数比较避免精度问题 assert w.balance pytest.approx(expected)代码解读与技巧Fixture (fresh_wallet) 这是pytest的精髓。它定义了一个“新鲜”的、初始状态确定余额100的钱包。每个使用了fresh_wallet参数的测试方法在开始前都会调用这个fixture函数获取一个新的实例。这保证了测试之间的独立性一个测试对钱包的修改不会影响另一个测试。这比在setUp方法里创建一个全局的、会被所有测试共享的self.wallet要安全得多。断言 直接使用Python的assert。当断言失败时pytest会输出非常清晰的对比信息例如assert a b失败它会告诉你a和b各自的值是什么。异常测试 (pytest.raises) 用with pytest.raises(异常类型)来包裹会抛出异常的代码。如果块内的代码没有抛出指定异常或者抛出了其他异常测试都会失败。match参数还可以用来验证异常信息是否包含特定字符串。参数化测试 (pytest.mark.parametrize) 这是避免写重复测试代码的神器。它允许你用一个测试函数测试多组输入输出数据。上面的例子中test_deposit_parametrized会被执行三次每次使用不同的(initial, deposit_amt, expected)数据。这极大地提高了测试的覆盖率和代码的简洁性。浮点数比较 金融计算涉及浮点数直接使用比较可能会因为精度问题失败。pytest.approx()提供了一个容忍度用于比较两个浮点数是否“足够接近”。3.3 测试具有依赖关系的复杂方法转账Wallet.transfer方法比单纯的存取款复杂因为它依赖于另一个Wallet对象target_wallet并且内部调用了withdraw和deposit。测试这类方法的关键是隔离和模拟Mock。我们先写一个不使用Mock的“集成”风格测试# 在 TestWallet 类中继续添加 def test_transfer_success(self): 测试转账成功流程 alice Wallet(Alice, 200.0) bob Wallet(Bob, 50.0) alice.transfer(bob, 75.0) assert alice.balance 125.0 # 200 - 75 assert bob.balance 125.0 # 50 75 # 可以进一步检查交易记录 assert len(alice.get_recent_transactions()) 0 assert alice.get_recent_transactions()[-1][type] transfer_out def test_transfer_insufficient_balance(self): 测试转账时余额不足 alice Wallet(Alice, 50.0) bob Wallet(Bob, 0.0) with pytest.raises(InsufficientBalanceError): alice.transfer(bob, 100.0) # Alice钱不够 # 确保转账失败后双方余额不变 assert alice.balance 50.0 assert bob.balance 0.0 def test_transfer_to_invalid_target(self): 测试转账目标非法 alice Wallet(Alice, 100.0) with pytest.raises(TypeError): alice.transfer(not_a_wallet_object, 10.0) # 目标不是Wallet实例这些测试是有效的但它们测试的是transfer方法与其依赖的withdraw和deposit方法的集成效果。如果withdraw本身有bugtest_transfer_success也会失败这不利于精准定位问题。单元测试的理想状态是只测试当前单元transfer方法的逻辑将其依赖视为已正确实现。这就需要用到Mock模拟。pytest通过pytest-mock插件或直接使用unittest.mock提供了强大的Mock功能。# 首先确保安装了pytest-mock: pip install pytest-mock # 然后在测试文件中使用 def test_transfer_logic_with_mock(self, mocker): # mocker是pytest-mock提供的fixture 使用Mock隔离测试transfer的核心逻辑 # 1. 创建两个Mock对象来模拟钱包 mock_source mocker.Mock(specWallet) # spec确保Mock具有Wallet的接口 mock_target mocker.Mock(specWallet) # 2. 为Mock对象配置属性和方法返回值 mock_source.balance 200.0 # 模拟属性 mock_source.withdraw.return_value 125.0 # 模拟withdraw方法调用返回125 mock_target.deposit.return_value 125.0 # 模拟deposit方法调用返回125 # 3. 创建真实的Wallet实例作为调用者但我们会替换它的方法 real_wallet Wallet(Caller, 0) # 我们直接测试transfer方法但为了Mock需要暂时替换其内部的_withdraw和_deposit调用吗 # 更清晰的做法是我们测试的是transfer调用了正确的依赖方法。 # 但我们的transfer直接调用了self.withdraw和target.deposit。 # 所以我们需要将real_wallet.withdraw和target_wallet.deposit替换为Mock。 # 使用mocker.patch.object来替换特定对象的方法 withdraw_mock mocker.patch.object(real_wallet, withdraw) deposit_mock mocker.patch.object(mock_target, deposit) # 执行转账 real_wallet.transfer(mock_target, 75.0) # 4. 验证断言 # 验证withdraw被以正确的参数调用了一次 withdraw_mock.assert_called_once_with(75.0) # 验证deposit被以正确的参数调用了一次 deposit_mock.assert_called_once_with(75.0) # 验证_record_transaction被调用了可能需要mock它这里略过 # 通过Mock我们完全隔离了withdraw和deposit的真实实现只关心transfer是否正确地“协调”了它们。Mock的使用心得Mock是一把双刃剑。过度使用Mock会让测试变得复杂且脱离实际。我的原则是优先测试真实集成当依赖项如数据库、网络API、复杂计算速度慢、不稳定或不可控时再使用Mock。对于Wallet.transfer因为它只依赖另一个内存中的对象且逻辑简单直接使用真实对象的集成测试test_transfer_success更直观、更有价值。Mock更适合测试“与外部服务交互”的逻辑比如测试一个“发送邮件”的函数你不需要真的发邮件只需Mock掉smtplib的相关调用验证函数是否用正确的参数调用了发送接口。3.4 测试私有方法与交易记录_record_transaction是一个私有方法以_开头。通常我们不直接对私有方法进行单元测试而是通过测试其调用的公有方法来间接覆盖它。get_recent_transactions是公有方法我们可以通过它来验证交易记录功能。# 在 TestWallet 类中继续添加 def test_transaction_history_after_operations(self, fresh_wallet): 测试一系列操作后的交易记录 w fresh_wallet initial_tx_count len(w._transaction_history) # 可以访问但不推荐作为常规断言 w.deposit(50) w.withdraw(20) w.deposit(10) recent_txs w.get_recent_transactions(3) assert len(recent_txs) 3 # 检查最后一条记录是最近一次存款 last_tx recent_txs[-1] assert last_tx[type] deposit assert last_tx[amount] 10.0 assert last_tx[balance_after] w.balance # 应该是 10050-2010 140 # 测试获取记录数大于实际数量 assert len(w.get_recent_transactions(10)) 4 # 初始1条创建钱包时可能记录这里我们的实现没有所以是3条操作记录 # 修正我们的实现中__init__不会调用_record_transaction所以只有3条。 # 让我们明确一下初始余额操作不记录所以是3条存款、取款、存款记录。 # 先获取全部记录看看 all_txs w.get_recent_transactions(10) assert len(all_txs) 3 assert all_txs[0][type] deposit and all_txs[0][amount] 50.0 assert all_txs[1][type] withdraw and all_txs[1][amount] 20.0 assert all_txs[2][type] deposit and all_txs[2][amount] 10.0 def test_get_recent_transactions_with_invalid_n(self, fresh_wallet): 测试get_recent_transactions传入无效参数 w fresh_wallet # 传入0或负数应返回空列表 assert w.get_recent_transactions(0) [] assert w.get_recent_transactions(-5) []这里我们发现了一个业务逻辑和测试的互动点__init__构造函数是否应该记录一条“开户”交易这取决于产品需求。我们的当前实现没有记录所以测试要基于此来写。如果未来需求变更__init__需要记录那么测试也需要相应更新。这正体现了测试驱动设计TDD的思想测试帮你思考接口的合理性。4. 高级技巧与最佳实践写完了基础测试我们来看看如何让测试更健壮、更高效。4.1 测试目录结构与命名约定一个清晰的结构有助于管理大量测试。your_project/ ├── src/ │ └── your_package/ │ ├── __init__.py │ ├── wallet.py # 生产代码 │ └── ... ├── tests/ # 测试目录 │ ├── __init__.py │ ├── conftest.py # 放置全局的fixture和配置 │ ├── unit/ # 单元测试 │ │ ├── __init__.py │ │ ├── test_wallet.py │ │ └── test_other.py │ └── integration/ # 集成测试 │ └── ... └── pyproject.toml / setup.cfgconftest.py 可以在这里定义项目范围内共享的fixture例如数据库连接、测试配置等。pytest会自动发现这个文件。测试文件命名test_*.py或*_test.py。测试类和函数命名 清晰描述测试内容如TestWallet,test_deposit_raises_error_on_negative_amount。4.2 使用Fixture管理测试生命周期和资源我们之前用了pytest.fixture来提供fresh_wallet。Fixture可以有不同作用域# 在 conftest.py 或 test_wallet.py 顶部 import pytest import tempfile import os pytest.fixture(scopesession) # 整个测试会话只执行一次 def database_connection(): 模拟一个数据库连接整个测试套件共用同一个 print(\n 建立模拟数据库连接Session级) conn {connected: True, session_id: sess_12345} yield conn # yield之前是setup之后是teardown print(\n 关闭模拟数据库连接Session级) conn[connected] False pytest.fixture(scopefunction) # 默认就是function每个测试函数执行一次 def temporary_file(): 为每个测试函数创建一个临时文件 with tempfile.NamedTemporaryFile(modew, deleteFalse, suffix.txt) as f: f.write(initial content) temp_path f.name yield temp_path # 将文件路径提供给测试函数 # 测试函数执行完毕后清理临时文件 os.unlink(temp_path) # 在测试中使用 def test_with_session_fixture(database_connection): assert database_connection[connected] is True def test_with_temp_file(temporary_file): assert os.path.exists(temporary_file) with open(temporary_file, r) as f: content f.read() assert initial content in content # 测试结束后temporary_file fixture会自动删除文件yieldFixture 这是管理需要清理的资源如文件、网络连接、数据库会话的最佳模式。yield之前的代码是设置yield返回资源给测试函数测试函数执行完后会回来执行yield之后的清理代码。4.3 测试覆盖率你的测试到底测了多少写了这么多测试我们怎么知道有没有测全这就需要测试覆盖率Code Coverage。覆盖率工具会分析你的测试执行了源代码的哪些行、哪些分支。安装覆盖率工具pip install pytest-cov运行测试并生成覆盖率报告# 运行测试并打印简略报告 pytest --covwallet tests/ # 生成详细的HTML报告便于查看哪些行没被覆盖 pytest --covwallet --cov-reporthtml tests/打开生成的htmlcov/index.html你可以清晰地看到wallet.py中每一行代码是否被测试执行过绿色表示已覆盖红色表示未覆盖。我们的目标是追求合理的、有意义的覆盖率而不是盲目追求100%。通常核心业务逻辑、条件分支、异常处理是覆盖的重点。像简单的属性访问器property或者某些极其简单的__init__方法如果逻辑简单到不可能出错不一定非要为了覆盖率而写测试。4.4 测试的“FIRST”原则与可维护性好的单元测试应该符合“FIRST”原则F - Fast快速 测试应该秒级完成。慢的测试会阻碍开发流程。避免在单元测试中做I/O操作文件、网络、数据库用Mock代替。I - Independent/Isolated独立/隔离 测试之间不应该有依赖关系可以以任何顺序运行。这正是我们使用fresh_wallet这样的fixture而不是共享类变量的原因。R - Repeatable可重复 在任何环境你的机器、同事的机器、CI服务器上运行都应该得到相同的结果。测试不能依赖外部状态如当前时间、网络状态所有输入都应明确给定。S - Self-Validating自我验证 测试结果应该是二元的通过/失败不需要人工检查日志或输出。这就是断言的作用。T - Timely及时 理想情况下测试代码应该与生产代码同时编写TDD最晚也应在代码提交前完成。5. 常见问题与排查技巧实录在实际编写和运行测试时你肯定会遇到各种问题。下面是我踩过的一些坑和解决方法。5.1 测试依赖与状态污染问题测试A创建了一个全局对象并修改了它导致测试B运行失败因为测试B假设该对象是初始状态。根因没有做好测试隔离。使用了模块级或类级的变量并在测试中修改了它们。解决始终使用fixture像我们例子中的fresh_wallet为每个测试提供全新的、独立的状态。避免setUpClass/tearDownClass中的可变状态如果使用unittest在setUpClass中初始化只读的常量或昂贵的资源如数据库连接池但不要初始化会被修改的数据。使用pytest的autousefixture进行清理对于某些全局清理可以定义一个autouseTrue的fixture。pytest.fixture(autouseTrue) def clear_global_cache(): 每个测试自动运行清理可能存在的全局缓存 from my_module import global_cache global_cache.clear() yield # 测试后也可以做一些清理5.2 浮点数比较失败问题assert wallet.balance 100.0失败因为balance计算后可能是99.99999999999999。根因计算机浮点数精度问题。解决使用pytest.approxassert wallet.balance pytest.approx(100.0)。使用math.iscloseassert math.isclose(wallet.balance, 100.0, rel_tol1e-9)。对于金融计算考虑使用Decimal类型在业务代码中使用decimal.Decimal代替float可以避免很多精度问题。但要注意Decimal运算比float慢。5.3 Mock对象行为不符合预期问题Mock了某个方法但测试中它没有被调用或者调用参数不对。根因Mock的时机不对或对象不对。排查确认Patch的目标mocker.patch的目标字符串必须是测试代码运行时看到的名字。如果生产代码是from utils.helper import send_email你在测试中就要patchutils.helper.send_email。如果生产代码是import utils.helper然后utils.helper.send_email()你就要patchutils.helper.send_email。如果生产代码是from utils import helper; helper.send_email()你就要patchutils.helper.send_email。理解Python的导入系统是关键。使用mocker.spy进行监视如果你不想完全替换一个函数只是想看看它被调用了没有、参数是什么可以用spy。def test_something(mocker): from my_module import some_function spy mocker.spy(my_module, some_function) # 监视原函数 # ... 执行测试代码 ... spy.assert_called_once_with(expected_arg) # 原函数some_function仍然会被正常执行打印Mock的调用记录在测试失败时可以打印mock_obj.call_args_list来查看这个Mock对象所有被调用的历史记录非常有用。5.4 测试在CI环境失败本地却通过问题在GitHub Actions, GitLab CI等持续集成环境中测试失败但在自己电脑上一切正常。根因环境差异。最常见的原因有依赖版本不同pip install时没有锁定版本CI环境安装了新版本的库其行为与旧版不同。缺少系统依赖某些Python包需要系统库如libpqforpsycopg2,libffi等CI镜像里可能没装。路径与权限测试中使用了绝对路径或者尝试写入没有权限的目录。随机性与时间测试中使用了随机数但没有固定种子或者依赖当前时间如datetime.now()CI运行时时间不同导致结果不同。解决使用requirements.txt或pyproject.toml精确指定依赖版本。在CI配置中安装系统依赖。使用临时目录和相对路径。Mock时间或使用固定种子# 在测试中Mock datetime.now def test_something(mocker): fixed_time datetime.datetime(2023, 10, 1, 12, 0, 0) datetime_mock mocker.patch(your_module.datetime) datetime_mock.now.return_value fixed_time # 现在你的代码里调用datetime.now()都会返回fixed_time5.5 测试运行太慢问题测试套件运行时间从几秒变成了几分钟严重影响开发效率。根因I/O操作测试中包含了真实的数据库查询、网络请求、文件读写。过度使用scopesession的fixture一个庞大的、初始化很慢的fixture被所有测试共享但可能只有少数测试需要它。测试数量膨胀没有合理组织测试重复测试类似场景。优化严格区分单元测试和集成测试单元测试必须Mock所有外部依赖。将需要真实I/O的测试标记为集成测试pytest.mark.integration并配置CI只在特定阶段运行它们。使用更小作用域的fixture默认用function级。只有创建成本极高的资源如数据库连接池、启动一个docker容器才用session级。使用pytest-xdist并行运行测试pytest -n autoauto表示使用所有CPU核心。定期清理无用测试删除那些测试已经不存在功能的代码或者合并重复的测试用例。单元测试不是一蹴而就的它是一个需要持续维护和优化的实践。一开始可能会觉得写测试拖慢了开发速度但当你经历过几次因为测试而提前发现重大bug或者能自信地重构代码而不用担心破坏现有功能时你就会深刻体会到“慢就是快”的道理。从今天这个“用户钱包”的例子开始为你下一个Python项目系上“安全带”吧。