类与对象:Python 的面向对象初体验,像搭积木一样写代码
文章目录
前提知识:函数入门:从"写重复代码"到"调用一次写十次用"、错误与异常:让程序不崩溃的完整指南
适合读者:掌握了函数与异常处理的 Python 入门开发者
函数解决了"代码重复"的问题,但随着程序越写越大,会碰上另一个麻烦:数据和操作这份数据的函数散落在各处,没有归属,改一个地方要牵动好几处,维护起来像在拆地雷。
面向对象(OOP)的思路是:把数据和操作数据的代码打包到一起,命名为"类"。从此,每一类事物有了自己的边界,改动只影响边界内部,不会到处传播。
本篇用一个银行账户系统作为贯穿场景:从一个最简单的账户类开始,逐步加功能,最后引入继承处理不同账户类型——每一步都在讲"为什么这样做",而不只是"怎么写"。
一、类的基本概念
1.1 类 vs 对象:图纸与成品
理解面向对象,最直观的类比是"图纸"和"按图纸造出来的东西":
类(Class)是模板,描述"这类事物有哪些属性、能做哪些操作"。
对象(Object)是按照这个模板创建出来的具体实例,每个对象有自己独立的数据。
一个类可以创建出成千上万个对象,就像一张图纸可以盖出无数栋同款房子,但每栋的住户、装修不同。
1.2 定义第一个类
class BankAccount:
"""银行账户类"""
def __init__(self, owner: str, initial_balance: float = 0.0):
self.owner = owner
self.balance = initial_balance
def deposit(self, amount: float) -> None:
self.balance += amount
print(f"存入 {amount:.2f} 元,当前余额:{self.balance:.2f} 元")
def withdraw(self, amount: float) -> bool:
if amount > self.balance:
print(f"余额不足,当前余额 {self.balance:.2f} 元,取款金额 {amount:.2f} 元")
return False
self.balance -= amount
print(f"取出 {amount:.2f} 元,当前余额:{self.balance:.2f} 元")
return True
def get_balance(self) -> float:
return self.balance
使用这个类:
alice = BankAccount("Alice", 5000.0)
bob = BankAccount("Bob", 1200.0)
alice.deposit(800) # 存入 800.00 元,当前余额:5800.00 元
bob.withdraw(500) # 取出 500.00 元,当前余额:700.00 元
bob.withdraw(1000) # 余额不足,当前余额 700.00 元,取款金额 1000.00 元
print(alice.get_balance()) # 5800.0
print(bob.get_balance()) # 700.0
二、__init__ 与 self 的本质
2.1 __init__ 是什么
__init__ 是初始化方法,在 BankAccount("Alice", 5000.0) 这行代码执行时自动调用,负责给新对象设置初始状态。
很多教程把它叫"构造函数",这个说法不完全准确——Python 真正"构造"对象的是 __new__,__init__ 只是初始化。但对入门阶段来说,把它理解为"创建对象时自动运行的函数"就够用了。
2.2 self 的本质
self 是面向对象初学者最常感到困惑的地方。来看一个对比:
# 不用类,用普通函数操作账户数据
def deposit(account_dict, amount):
account_dict["balance"] += amount
account = {"owner": "Alice", "balance": 5000.0}
deposit(account, 800) # 必须显式传入 account
# 用类,方法自动知道在操作哪个对象
class BankAccount:
def deposit(self, amount):
self.balance += amount
alice = BankAccount("Alice", 5000.0)
alice.deposit(800) # Python 自动把 alice 作为第一个参数传进去
self 就是"把当前对象自动传进来"的机制。alice.deposit(800) 在 Python 内部等价于 BankAccount.deposit(alice, 800)——只不过这个传递是隐式的,不需要手动写。
# 这两行代码完全等价
alice.deposit(800)
BankAccount.deposit(alice, 800)
self 这个名字只是约定,换成 this 或任何其他名字语法上都对,但绝对不推荐这样做。
2.3 实例属性 vs 类属性
这是一个容易踩的坑:
class BankAccount:
bank_name = "Python 银行" # 类属性:所有对象共享
transaction_count = 0 # 类属性:记录总交易次数(危险!见下)
def __init__(self, owner, balance=0.0):
self.owner = owner # 实例属性:每个对象独立
self.balance = balance # 实例属性:每个对象独立
alice = BankAccount("Alice", 5000.0)
bob = BankAccount("Bob", 1200.0)
# 访问类属性
print(alice.bank_name) # "Python 银行"
print(bob.bank_name) # "Python 银行"
# 修改类属性(通过类名修改,影响所有实例)
BankAccount.bank_name = "工商银行"
print(alice.bank_name) # "工商银行"
print(bob.bank_name) # "工商银行"
# ⚠️ 用实例名修改,实际上是在实例上创建了一个同名的实例属性
alice.bank_name = "建设银行" # 给 alice 创建了实例属性,不影响类属性和 bob
print(alice.bank_name) # "建设银行"(alice 自己的实例属性)
print(bob.bank_name) # "工商银行"(类属性,没被影响)
print(BankAccount.bank_name) # "工商银行"(类属性,没被影响)
属性查找顺序:Python 查找属性时,先找实例属性,再找类属性,再往父类找。一旦实例上有同名属性,类属性就被"遮蔽"了。
三、方法的几种形态
3.1 实例方法(最常用)
普通方法,第一个参数是 self,操作的是具体实例的数据:
class BankAccount:
def deposit(self, amount: float) -> None: # 实例方法
self.balance += amount
3.2 类方法
用 @classmethod 装饰,第一个参数是 cls(类本身,而不是实例),常用于提供"备用构造函数":
class BankAccount:
def __init__(self, owner: str, balance: float = 0.0):
self.owner = owner
self.balance = balance
@classmethod
def from_dict(cls, data: dict) -> "BankAccount":
"""从字典创建账户(备用构造函数)"""
return cls(data["owner"], data.get("balance", 0.0))
# 两种创建方式都合法
account1 = BankAccount("Alice", 5000.0)
account2 = BankAccount.from_dict({"owner": "Bob", "balance": 1200.0})
3.3 静态方法
用 @staticmethod 装饰,不接收 self 也不接收 cls,功能上与普通函数无异,只是放在类里是为了逻辑归属:
class BankAccount:
@staticmethod
def validate_amount(amount: float) -> bool:
"""校验金额是否合法(与具体实例无关)"""
return isinstance(amount, (int, float)) and amount > 0
三种方法的对比:
| 方法类型 | 装饰器 | 第一个参数 | 能访问实例数据? | 能访问类数据? | 典型用途 |
|---|---|---|---|---|---|
| 实例方法 | 无 | self |
✅ | ✅ | 操作对象状态 |
| 类方法 | @classmethod |
cls |
❌ | ✅ | 备用构造函数、工厂方法 |
| 静态方法 | @staticmethod |
无 | ❌ | ❌ | 工具函数,逻辑归属于类 |
四、__str__ 与 __repr__
4.1 为什么需要它们
alice = BankAccount("Alice", 5000.0)
print(alice)
# 默认输出:<__main__.BankAccount object at 0x000001A2B3C4D5E6>
这个默认输出没什么实际价值,定义 __str__ 和 __repr__ 可以让对象"开口说话"。
4.2 两者的区别
class BankAccount:
def __init__(self, owner: str, balance: float = 0.0):
self.owner = owner
self.balance = balance
def __str__(self) -> str:
"""给人看的,print() 和 str() 调用它"""
return f"账户持有人:{self.owner},余额:{self.balance:.2f} 元"
def __repr__(self) -> str:
"""给开发者看的,调试器和交互式解释器调用它"""
return f"BankAccount(owner={self.owner!r}, balance={self.balance!r})"
alice = BankAccount("Alice", 5000.0)
print(alice) # 账户持有人:Alice,余额:5000.00 元 (调用 __str__)
str(alice) # '账户持有人:Alice,余额:5000.00 元'
repr(alice) # "BankAccount(owner='Alice', balance=5000.0)" (调用 __repr__)
# 在列表里,print([alice]) 调用的是 __repr__ 而不是 __str__
accounts = [alice]
print(accounts) # [BankAccount(owner='Alice', balance=5000.0)]
设计原则:
__str__:面向终端用户,读起来舒服,格式友好__repr__:面向开发者,最好能直接复制进 Python 解释器重现这个对象,即eval(repr(obj)) == obj- 如果只定义一个,优先定义
__repr__——str()在找不到__str__时会退而求其次调用__repr__
五、继承:让不同类型共享共同逻辑
5.1 为什么需要继承
银行有多种账户类型:普通储蓄账户、有利息的定期账户、有透支额度的信用账户。它们都有"持有人"、“余额”、"存款"这些共同点,但取款逻辑各不相同。
如果每种账户都从零开始写,公共逻辑就会重复三遍——这正是继承要解决的问题。
5.2 基类定义
class BankAccount:
"""银行账户基类"""
def __init__(self, owner: str, balance: float = 0.0):
self.owner = owner
self._balance = balance # 前缀 _ 表示"内部属性,不建议外部直接访问"
def deposit(self, amount: float) -> None:
if not BankAccount._is_valid_amount(amount):
raise ValueError(f"存款金额必须为正数,收到 {amount}")
self._balance += amount
print(f"[{self.owner}] 存入 {amount:.2f} 元,余额:{self._balance:.2f} 元")
def withdraw(self, amount: float) -> bool:
"""取款,子类可重写此方法"""
raise NotImplementedError("子类必须实现 withdraw 方法")
def get_balance(self) -> float:
return self._balance
@staticmethod
def _is_valid_amount(amount: float) -> bool:
return isinstance(amount, (int, float)) and amount > 0
def __str__(self) -> str:
return f"{self.__class__.__name__}[{self.owner}] 余额:{self._balance:.2f} 元"
def __repr__(self) -> str:
return f"{self.__class__.__name__}(owner={self.owner!r}, balance={self._balance!r})"
5.3 子类:储蓄账户
class SavingsAccount(BankAccount):
"""储蓄账户:余额不足时拒绝取款"""
def __init__(self, owner: str, balance: float = 0.0):
super().__init__(owner, balance) # 调用父类 __init__,初始化公共属性
self.withdrawal_count = 0 # 新增属性:记录取款次数
def withdraw(self, amount: float) -> bool:
"""重写取款逻辑:余额不足则拒绝"""
if not BankAccount._is_valid_amount(amount):
raise ValueError(f"取款金额必须为正数,收到 {amount}")
if amount > self._balance:
print(f"[{self.owner}] 余额不足({self._balance:.2f} 元),无法取出 {amount:.2f} 元")
return False
self._balance -= amount
self.withdrawal_count += 1
print(f"[{self.owner}] 取出 {amount:.2f} 元,余额:{self._balance:.2f} 元"
f"(本月取款 {self.withdrawal_count} 次)")
return True
5.4 子类:信用账户
class CreditAccount(BankAccount):
"""信用账户:可在信用额度范围内透支"""
def __init__(self, owner: str, balance: float = 0.0, credit_limit: float = 5000.0):
super().__init__(owner, balance)
self.credit_limit = credit_limit # 新增属性:信用额度
def withdraw(self, amount: float) -> bool:
"""重写取款逻辑:可透支到信用额度"""
if not BankAccount._is_valid_amount(amount):
raise ValueError(f"取款金额必须为正数,收到 {amount}")
available = self._balance + self.credit_limit
if amount > available:
print(f"[{self.owner}] 超出可用额度(余额 {self._balance:.2f} + "
f"信用额度 {self.credit_limit:.2f} = {available:.2f} 元)")
return False
self._balance -= amount
status = "(使用信用额度)" if self._balance < 0 else ""
print(f"[{self.owner}] 取出 {amount:.2f} 元,余额:{self._balance:.2f} 元{status}")
return True
def __str__(self) -> str:
return (f"CreditAccount[{self.owner}] "
f"余额:{self._balance:.2f} 元,信用额度:{self.credit_limit:.2f} 元")
5.5 super() 的调用链
super() 不只是"调用父类方法",它遵循 Python 的 MRO(方法解析顺序,Method Resolution Order):
# 查看一个类的 MRO
print(SavingsAccount.__mro__)
# (<class 'SavingsAccount'>, <class 'BankAccount'>, <class 'object'>)
调用链:SavingsAccount.__init__ → super().__init__ → BankAccount.__init__ → object.__init__
六、实战:银行账户系统
把所有内容整合起来,加上异常处理和一个简单的账户管理器:
import re
from typing import Optional
# ── 自定义异常 ────────────────────────────────────
class BankError(Exception):
pass
class InsufficientFundsError(BankError):
def __init__(self, owner: str, requested: float, available: float):
self.owner = owner
self.requested = requested
self.available = available
super().__init__(
f"[{owner}] 可用余额 {available:.2f} 元,取款金额 {requested:.2f} 元"
)
# ── 账户基类 ──────────────────────────────────────
class BankAccount:
def __init__(self, owner: str, balance: float = 0.0):
if not owner or not isinstance(owner, str):
raise ValueError("账户持有人姓名不能为空")
self.owner = owner.strip()
self._balance = max(0.0, float(balance))
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError(f"存款金额必须大于 0,收到 {amount}")
self._balance += amount
def withdraw(self, amount: float) -> None:
raise NotImplementedError
def get_balance(self) -> float:
return self._balance
def __str__(self) -> str:
return f"{self.__class__.__name__}[{self.owner}] ¥{self._balance:.2f}"
def __repr__(self) -> str:
return f"{self.__class__.__name__}(owner={self.owner!r}, balance={self._balance!r})"
# ── 储蓄账户 ──────────────────────────────────────
class SavingsAccount(BankAccount):
def withdraw(self, amount: float) -> None:
if amount <= 0:
raise ValueError(f"取款金额必须大于 0,收到 {amount}")
if amount > self._balance:
raise InsufficientFundsError(self.owner, amount, self._balance)
self._balance -= amount
# ── 信用账户 ──────────────────────────────────────
class CreditAccount(BankAccount):
def __init__(self, owner: str, balance: float = 0.0, credit_limit: float = 5000.0):
super().__init__(owner, balance)
self.credit_limit = float(credit_limit)
def withdraw(self, amount: float) -> None:
if amount <= 0:
raise ValueError(f"取款金额必须大于 0,收到 {amount}")
available = self._balance + self.credit_limit
if amount > available:
raise InsufficientFundsError(self.owner, amount, available)
self._balance -= amount
def __str__(self) -> str:
return (f"CreditAccount[{self.owner}] "
f"¥{self._balance:.2f}(信用额度 ¥{self.credit_limit:.2f})")
# ── 账户管理器 ────────────────────────────────────
class AccountManager:
"""管理多个账户,支持转账操作"""
def __init__(self):
self._accounts: dict[str, BankAccount] = {}
def add_account(self, account: BankAccount) -> None:
if account.owner in self._accounts:
raise BankError(f"账户 '{account.owner}' 已存在")
self._accounts[account.owner] = account
print(f"账户已创建:{account}")
def get_account(self, owner: str) -> Optional[BankAccount]:
return self._accounts.get(owner)
def transfer(self, from_owner: str, to_owner: str, amount: float) -> None:
"""转账:从 from_owner 取款,存入 to_owner"""
src = self._accounts.get(from_owner)
dst = self._accounts.get(to_owner)
if src is None:
raise BankError(f"转出账户 '{from_owner}' 不存在")
if dst is None:
raise BankError(f"转入账户 '{to_owner}' 不存在")
src.withdraw(amount) # 可能抛出 InsufficientFundsError
dst.deposit(amount)
print(f"转账成功:{from_owner} → {to_owner},金额 ¥{amount:.2f}")
def list_accounts(self) -> None:
print("\n── 账户列表 ──")
for acc in self._accounts.values():
print(f" {acc}")
print("─────────────\n")
# ── 演示 ──────────────────────────────────────────
manager = AccountManager()
manager.add_account(SavingsAccount("Alice", 5000.0))
manager.add_account(SavingsAccount("Bob", 1200.0))
manager.add_account(CreditAccount("Carol", 300.0, credit_limit=3000.0))
manager.list_accounts()
# 正常操作
manager.transfer("Alice", "Bob", 800.0)
# 尝试超额取款
try:
manager.transfer("Bob", "Alice", 5000.0)
except InsufficientFundsError as e:
print(f"转账失败:{e}")
# Carol 使用信用额度
carol = manager.get_account("Carol")
carol.withdraw(2000.0) # 余额 300 + 信用 3000 = 3300,可以取
print(carol)
manager.list_accounts()
运行结果:
账户已创建:SavingsAccount[Alice] ¥5000.00
账户已创建:SavingsAccount[Bob] ¥1200.00
账户已创建:CreditAccount[Carol] ¥300.00(信用额度 ¥3000.00)
── 账户列表 ──
SavingsAccount[Alice] ¥5000.00
SavingsAccount[Bob] ¥1200.00
CreditAccount[Carol] ¥300.00(信用额度 ¥3000.00)
─────────────
转账成功:Alice → Bob,金额 ¥800.00
转账失败:[Bob] 可用余额 2000.00 元,取款金额 5000.00 元
CreditAccount[Carol] ¥-1700.00(信用额度 ¥3000.00)
── 账户列表 ──
SavingsAccount[Alice] ¥4200.00
SavingsAccount[Bob] ¥2000.00
CreditAccount[Carol] ¥-1700.00(信用额度 ¥3000.00)
─────────────
七、常见问题与易错点
7.1 __init__ 里忘记调用 super()
class SavingsAccount(BankAccount):
def __init__(self, owner, balance=0.0):
# ❌ 忘记 super().__init__()
self.withdrawal_count = 0
# 此时 self.owner 和 self._balance 根本没有被设置!
# 调用 deposit() 会立刻报 AttributeError
解决:子类的 __init__ 里,如果要添加新属性,先调 super().__init__(),再设置新属性。
7.2 方法漏写 self
class BankAccount:
def deposit(amount): # ❌ 漏写 self
self.balance += amount # NameError: name 'self' is not defined
self 不是可选的,每个实例方法的第一个参数都必须是它(名字约定为 self,不是强制,但绝对别改)。
7.3 直接修改"应该保护的"属性
alice = BankAccount("Alice", 5000.0)
alice._balance = 999999999 # 语法上合法,但绕开了所有校验
Python 的单下划线前缀 _ 是君子协定,不是真正的访问控制。它表达的意思是:“这个属性是内部实现细节,外部代码不应该直接修改”。如果需要更强的保护,可以用双下划线 __balance 触发名称改写(name mangling),但入门阶段用单下划线约定就够了。
7.4 类属性被实例属性意外遮蔽
如"实例属性 vs 类属性"一节所述——alice.bank_name = "x" 不会改类属性,而是给 alice 单独创建了一个同名实例属性。这个坑在用可变类属性(如列表)时更危险:
class BankAccount:
transaction_history = [] # ❌ 可变类属性:所有实例共享同一个列表!
def deposit(self, amount):
self.transaction_history.append(f"存入 {amount}") # 修改了类属性!
alice = BankAccount("Alice", 5000.0)
bob = BankAccount("Bob", 1200.0)
alice.deposit(100)
print(bob.transaction_history) # ['存入 100'] —— bob 也能看到 alice 的交易!
正确写法:可变属性必须放在 __init__ 里用 self. 初始化,每个实例独立拥有:
class BankAccount:
def __init__(self, owner, balance=0.0):
self.owner = owner
self._balance = balance
self.transaction_history = [] # ✅ 实例属性,每个对象独立
八、面向对象核心知识结构
九、类的设计不是教条,是权衡
不是所有代码都需要用类来组织。Python 的哲学是"实用优先":
- 几个相关函数 + 一份数据 → 直接用函数 + 字典,不必非要类
- 数据有状态,操作这个状态的逻辑有多处 → 用类,封装比裸字典更清晰
- 同类事物有多种变体,共享大部分逻辑但部分行为不同 → 用继承
银行账户之所以适合用类:余额是状态,必须经过存取款方法修改(不能随意赋值);不同账户类型(储蓄/信用)共享大部分逻辑但取款规则不同——这两点天然契合类和继承的设计意图。
而[函数入门:从"写重复代码"到"调用一次写十次用"]里的计算器,只有数值输入和结果输出,没有需要保护的状态,函数足矣——过早引入类反而会让代码更复杂。
如果这篇文章把面向对象讲得稍微清楚了一点点,欢迎点赞收藏——每一个点赞都是继续写下去的理由。
系列里前几篇的基础都在这里用上了:错误与异常:让程序不崩溃的完整指南 中的自定义异常在银行系统里直接派上用场,函数入门:从"写重复代码"到"调用一次写十次用" 中讲过的参数规范在方法定义里同样适用。如果某个概念看起来眼熟,那是因为它本来就是一脉相承的。
关注专栏,Python 基础系列持续更新中。
更多推荐




所有评论(0)