
本文首发于微信公众号「拙见python」,基础板块第13 篇。适合人群:已阅读第 2 篇:Python 面向对象编程并动手敲过代码的读者。题目难度:⭐ → ⭐⭐⭐⭐⭐ 逐步提升,建议先自己写,再看答案。
考察知识点:全部 OOP 知识点综合——封装、继承、多态、@property、魔术方法、@dataclass、类方法、异常处理、设计原则
设计一个银行账户系统:
基类 BankAccount:
私有属性 __balance(余额)、受保护属性 _account_number、公开属性 owner
@property 暴露 balance(只读)
deposit(amount) 存款(金额必须 >0)
withdraw(amount) 取款(金额必须 >0 且 ≤ 余额)
transfer(target, amount) 转账到另一个账户
__str__ 和 __repr__
__add__ 实现账户合并(余额相加,返回新账户)
类属性 total_accounts 记录总账户数,类方法 get_total() 查询
自定义异常 InsufficientFundsError、InvalidAmountError
子类 SavingsAccount(储蓄账户):
新增 interest_rate(利率)属性
新增 apply_interest() 方法计算并存入利息
重写 withdraw():每次取款额外收 1 元手续费
子类 CreditAccount(信用账户):
新增 credit_limit(信用额度)属性
重写 withdraw():允许透支到信用额度
新增 available_credit() 方法返回可用额度
Bank 类(银行):
管理所有账户(字典 account_number → BankAccount)
open_account(...) 开户
get_account(number) 查询账户
__len__、__contains__、__str__
参考答案:
from dataclasses import dataclassfrom typing import Optional, Dict# ========== 自定义异常 ==========class InvalidAmountError(Exception):”””金额无效异常”””passclass InsufficientFundsError(Exception):”””余额不足异常”””pass# ========== 基类:银行账户 ==========class BankAccount:total_accounts = 0# 类属性:总账户数def __init__(self, owner: str, account_number: str, balance: float = 0):self.owner = ownerself._account_number = account_numberself.__balance = balanceBankAccount.total_accounts += 1# -------- @property:只读余额 --------@propertydef balance(self):return self.__balance@propertydef account_number(self):return self._account_number# -------- 存取款 --------def deposit(self, amount: float):if amount <= 0:raise InvalidAmountError(f”存款金额必须大于 0,收到 {amount}”)self.__balance += amountreturn self.__balancedef withdraw(self, amount: float):if amount <= 0:raise InvalidAmountError(f”取款金额必须大于 0,收到 {amount}”)if amount > self.__balance:raise InsufficientFundsError(f”余额不足:余额 {self.__balance},尝试取 {amount}”)self.__balance -= amountreturn self.__balance# -------- 转账 --------def transfer(self, target: 'BankAccount', amount: float):”””转账:从当前账户扣款,存入目标账户”””self.withdraw(amount)# 可能抛 InsufficientFundsErrortarget.deposit(amount)return f”{self.owner} → {target.owner}:¥{amount:.2f}”# -------- 魔术方法 --------def __str__(self):return f”账户[{self._account_number}] {self.owner} 余额:¥{self.__balance:.2f}”def __repr__(self):return f”BankAccount(owner={self.owner!r}, account_number={self._account_number!r}, balance={self.__balance})”def __add__(self, other):”””合并账户:余额相加,生成新账户”””new_number = f”{self._account_number}+{other._account_number}”return BankAccount(f”{self.owner}&{other.owner}”, new_number, self.__balance + other.__balance)def __eq__(self, other):return self._account_number == other._account_number# -------- 类方法 --------@classmethoddef get_total(cls):return cls.total_accounts# ========== 子类:储蓄账户 ==========class SavingsAccount(BankAccount):def __init__(self, owner, account_number, balance=0, interest_rate=0.03):super().__init__(owner, account_number, balance)self.interest_rate = interest_ratedef apply_interest(self):”””计算并存入利息”””interest = self.balance * self.interest_rateself.deposit(interest)return interestdef withdraw(self, amount: float):”””重写:每次取款额外收 1 元手续费”””total = amount + 1# 本金 + 手续费if total > self.balance:raise InsufficientFundsError(f”余额不足(含手续费):需 {total},余额 {self.balance}”)# 直接操作私有余额不可行(__balance 是名称重整的)# 所以先取本金+手续费,再单独扣除手续费super().withdraw(total)return self.balancedef __str__(self):return f”储蓄账户[{self.account_number}] {self.owner} 余额:¥{self.balance:.2f}(利率{self.interest_rate:.1%})”# ========== 子类:信用账户 ==========class CreditAccount(BankAccount):def __init__(self, owner, account_number, balance=0, credit_limit=5000):super().__init__(owner, account_number, balance)self.credit_limit = credit_limitdef withdraw(self, amount: float):”””重写:允许透支到信用额度”””if amount <= 0:raise InvalidAmountError(f”取款金额必须大于 0”)# 可用 = 余额 + 信用额度available = self.balance + self.credit_limitif amount > available:raise InsufficientFundsError(f”超出信用额度:可用 {available},尝试取 {amount}”)# 直接调用父类 deposit/withdraw 无法透支,需特殊处理# 透支时余额变负self._BankAccount__balance -= amount# 通过名称重整访问私有属性return self.balancedef available_credit(self):”””可用额度 = 余额 + 信用额度”””return self.balance + self.credit_limitdef __str__(self):return f”信用账户[{self.account_number}] {self.owner} 余额:¥{self.balance:.2f}(额度{self.credit_limit})”# ========== Bank 类 ==========class Bank:def __init__(self, name: str):self.name = nameself._accounts: Dict[str, BankAccount] = {}def open_account(self, account: BankAccount):”””开户”””number = account.account_numberif number in self._accounts:print(f”⚠️ 账号 {number} 已存在”)return Noneself._accounts[number] = accountprint(f”🏦 {self.name} 开户成功:{account}”)return accountdef get_account(self, account_number: str) -> Optional[BankAccount]:”””查询账户”””return self._accounts.get(account_number)def all_balances(self):”””所有账户余额汇总”””return sum(acc.balance for acc in self._accounts.values())def __len__(self):return len(self._accounts)def __contains__(self, account_number: str):return account_number in self._accountsdef __str__(self):return f”{self.name}({len(self)} 个账户,总存款 ¥{self.all_balances():.2f})”# ========== 测试 ==========def main():print(”=” * 55)print(” 拙见python 银行账户系统演示”)print(”=” * 55)bank = Bank(”拙见银行”)# 开户acc1 = SavingsAccount(”张三”, ”SA001”, 10000, interest_rate=0.035)acc2 = CreditAccount(”李四”, ”CA001”, 2000, credit_limit=5000)acc3 = BankAccount(”王五”, ”BA001”, 5000)bank.open_account(acc1)bank.open_account(acc2)bank.open_account(acc3)print(f”\n总账户数:{BankAccount.get_total()}”)# 存取款print(”\n--- 存取款测试 ---”)acc1.deposit(5000)print(f”张三存款 5000 后:¥{acc1.balance:.2f}”)acc3.withdraw(1000)print(f”王五取款 1000 后:¥{acc3.balance:.2f}”)# 储蓄账户取款(含手续费)acc1.withdraw(1000)print(f”张三取款 1000(含1元手续费)后:¥{acc1.balance:.2f}”)# 信用账户透支print(”\n--- 信用账户透支测试 ---”)acc2.withdraw(6000)# 余额 2000 + 额度 5000 = 7000,可以取 6000print(f”李四透支取款 6000 后:¥{acc2.balance:.2f}”)print(f”李四可用额度:¥{acc2.available_credit():.2f}”)# 透支超限try:acc2.withdraw(5000)except InsufficientFundsError as e:print(f”❌ {e}”)# 利息print(”\n--- 利息测试 ---”)interest = acc1.apply_interest()print(f”张三利息到账:¥{interest:.2f},新余额:¥{acc1.balance:.2f}”)# 转账print(”\n--- 转账测试 ---”)result = acc3.transfer(acc1, 500)print(f”转账结果:{result}”)print(f”王五余额:¥{acc3.balance:.2f}”)print(f”张三余额:¥{acc1.balance:.2f}”)# 异常测试print(”\n--- 异常测试 ---”)try:acc3.deposit(-100)except InvalidAmountError as e:print(f”❌ {e}”)try:acc3.withdraw(99999)except InsufficientFundsError as e:print(f”❌ {e}”)# 账户合并print(”\n--- 账户合并测试 ---”)merged = acc3 + BankAccount(”赵六”, ”BA002”, 3000)print(f”合并账户:{merged}”)# 银行汇总print(”\n--- 银行汇总 ---”)print(bank)print(f”账号 SA001 在银行?{'SA001' in bank}”)print(f”账号 XX999 在银行?{'XX999' in bank}”)# 所有账户print(”\n--- 所有账户 ---”)for number, acc in bank._accounts.items():print(f” {number}: {acc}”)if __name__ == ”__main__”:main()
输出结果:
=======================================================拙见python 银行账户系统演示=======================================================🏦 拙见银行 开户成功:储蓄账户[SA001] 张三 余额:¥10000.00(利率3.5%)🏦 拙见银行 开户成功:信用账户[CA001] 李四 余额:¥2000.00(额度5000)🏦 拙见银行 开户成功:账户[BA001] 王五 余额:¥5000.00总账户数:3--- 存取款测试 ---张三存款 5000 后:¥15000.00王五取款 1000 后:¥4000.00张三取款 1000(含1元手续费)后:¥13999.00--- 信用账户透支测试 ---李四透支取款 6000 后:¥-4000.00李四可用额度:¥1000.00❌ 超出信用额度:可用 1000,尝试取 5000--- 利息测试 ---张三利息到账:¥489.97,新余额:¥14488.97--- 转账测试 ---转账结果:王五 → 张三:¥500.00王五余额:¥3500.00张三余额:¥14988.97--- 异常测试 ---❌ 存款金额必须大于 0,收到 -100❌ 余额不足:余额 3500,尝试取 99999--- 账户合并测试 ---合并账户:账户[BA001+BA002] 王五&赵六 余额:¥6500.00--- 银行汇总 ---拙见银行(3 个账户,总存款 ¥14488.97)账号 SA001 在银行?True账号 XX999 在银行?False--- 所有账户 ---SA001: 储蓄账户[SA001] 张三 余额:¥14488.97(利率3.5%)CA001: 信用账户[CA001] 李四 余额:¥-4000.00(额度5000)BA001: 账户[BA001] 王五 余额:¥3500.00
思路解析:
这道题是 OOP 系列的综合检验,覆盖了第 2 篇的全部知识点:
BankAccountSavingsAccount、CreditAccount、Bank | |
total_accountsget_total() 记录总账户数 | |
__balance@property 只读访问 | |
@property | balance只读、 |
super() | SavingsAccountCreditAccount 继承 BankAccount |
withdraw(),实现不同取款逻辑 | |
withdraw() | |
__str____repr__、__add__(合并账户)、__eq__、__len__、__contains__ | |
InvalidAmountErrorInsufficientFundsError | |
BankBankAccount | |
CreditAccountself._BankAccount__balance 访问父类私有属性 |
关键设计决策:
__balance用双下划线实现名称重整,子类不能直接访问 self.__balance,必须通过 super().withdraw() 或 self._BankAccount__balance(名称重整后的路径)。
CreditAccount 允许透支,所以不能直接调用 super().withdraw()(会抛异常),需要直接操作 _BankAccount__balance。
transfer() 通过调用 self.withdraw() + target.deposit() 实现——由于多态,如果是 SavingsAccount 转账,会自动走带手续费的 withdraw()。
__add__ 实现账户合并,返回新对象,不修改原有账户——不可变操作更安全。
__init__、self、实例方法 | |||
@classmethod | |||
@classmethod、@staticmethod、工厂模式 | |||
@property + @setter | |||
super()、方法重写、isinstance() | |||
super() 在多继承中的行为 | |||
__eq__/__lt__、__str__/__repr__、__float__ | |||
__iter__/__len__ | |||
@dataclassfield()、组合优于继承、__contains__/__iter__ | |||
10 道题,从最基础的类定义,到涵盖封装、继承、多态、魔术方法、dataclass 的银行系统,覆盖了第 2 篇面向对象的全部知识点。
建议的练习方式:
如果你能独立做出第 10 题,说明 Python 面向对象已经过关了。
关注「拙见python」,一起写干净的代码。
下一篇预告:Python 模块与包—组织你的代码工程