前提知识:函数入门:从"写重复代码"到"调用一次写十次用"错误与异常:让程序不崩溃的完整指南
适合读者:掌握了函数与异常处理的 Python 入门开发者


函数解决了"代码重复"的问题,但随着程序越写越大,会碰上另一个麻烦:数据和操作这份数据的函数散落在各处,没有归属,改一个地方要牵动好几处,维护起来像在拆地雷。

面向对象(OOP)的思路是:把数据操作数据的代码打包到一起,命名为"类"。从此,每一类事物有了自己的边界,改动只影响边界内部,不会到处传播。

本篇用一个银行账户系统作为贯穿场景:从一个最简单的账户类开始,逐步加功能,最后引入继承处理不同账户类型——每一步都在讲"为什么这样做",而不只是"怎么写"。


一、类的基本概念

1.1 类 vs 对象:图纸与成品

理解面向对象,最直观的类比是"图纸"和"按图纸造出来的东西":

创建(实例化)

创建(实例化)

对象:bob_account

owner = 'Bob'

balance = 1200.0

对象:alice_account

owner = 'Alice'

balance = 5000.0

类(图纸)BankAccount

属性:owner, balance

方法:deposit, withdraw, get_balance

(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 查找属性时,先找实例属性,再找类属性,再往父类找。一旦实例上有同名属性,类属性就被"遮蔽"了。

没有

没有

没有

alice.bank_name

实例属性中
有 bank_name?

返回实例属性值
'建设银行'

类属性中
有 bank_name?

返回类属性值
'工商银行'

父类中有?

返回父类属性值

抛出 AttributeError


三、方法的几种形态

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 为什么需要继承

银行有多种账户类型:普通储蓄账户、有利息的定期账户、有透支额度的信用账户。它们都有"持有人"、“余额”、"存款"这些共同点,但取款逻辑各不相同。

如果每种账户都从零开始写,公共逻辑就会重复三遍——这正是继承要解决的问题。

BankAccount(基类)
owner, balance
deposit, get_balance, __str__

SavingsAccount(储蓄账户)
继承所有属性和方法
重写 withdraw:余额不足时直接拒绝

CreditAccount(信用账户)
新增 credit_limit 属性
重写 withdraw:可透支到信用额度

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__

super().__init__(owner, balance)

隐式调用

SavingsAccount.__init__
(owner, balance)

BankAccount.__init__
设置 owner, _balance

object.__init__
Python 所有类的根


六、实战:银行账户系统

把所有内容整合起来,加上异常处理和一个简单的账户管理器:

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 = []   # ✅ 实例属性,每个对象独立

八、面向对象核心知识结构

面向对象(OOP)

类的定义
class + __init__ + self

属性
实例属性 vs 类属性

方法
实例方法 / 类方法 / 静态方法

字符串表示
__str__ vs __repr__

继承
super() + 方法重写

self 本质:隐式传递的第一个参数
alice.deposit(x) = BankAccount.deposit(alice, x)

查找顺序:实例 → 类 → 父类

可变类属性的陷阱
必须用 self. 初始化

@classmethod:备用构造函数

@staticmethod:逻辑归属,无 self

__str__:面向用户,print() 调用

__repr__:面向开发者,调试器调用

super().__init__() 不能忘

raise NotImplementedError
强制子类实现接口


九、类的设计不是教条,是权衡

不是所有代码都需要用类来组织。Python 的哲学是"实用优先":

  • 几个相关函数 + 一份数据 → 直接用函数 + 字典,不必非要类
  • 数据有状态,操作这个状态的逻辑有多处 → 用类,封装比裸字典更清晰
  • 同类事物有多种变体,共享大部分逻辑但部分行为不同 → 用继承

银行账户之所以适合用类:余额是状态,必须经过存取款方法修改(不能随意赋值);不同账户类型(储蓄/信用)共享大部分逻辑但取款规则不同——这两点天然契合类和继承的设计意图。

而[函数入门:从"写重复代码"到"调用一次写十次用"]里的计算器,只有数值输入和结果输出,没有需要保护的状态,函数足矣——过早引入类反而会让代码更复杂。


如果这篇文章把面向对象讲得稍微清楚了一点点,欢迎点赞收藏——每一个点赞都是继续写下去的理由。

系列里前几篇的基础都在这里用上了:错误与异常:让程序不崩溃的完整指南 中的自定义异常在银行系统里直接派上用场,函数入门:从"写重复代码"到"调用一次写十次用" 中讲过的参数规范在方法定义里同样适用。如果某个概念看起来眼熟,那是因为它本来就是一脉相承的。

关注专栏,Python 基础系列持续更新中。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐