一次关于 Python 面向对象、只读属性、raise 与异常处理的学习记录

这是我在最近复习python语法的时候,偶然看到的去年的学习笔记,感觉挺有用的,所以发在自己的博客里希望对大家有点帮助,正片如下:

最近在练习 Python 面向对象的时候,我写了一个 BankAccount 类。本来只是想完成一个很基础的练习:定义账号、余额、存钱、取钱这些功能,结果在这个过程中,我把好几个很关键的知识点都串起来了,包括:

  • 类的属性和方法怎么写
  • 什么叫“只读属性”
  • deposit()withdraw() 的业务逻辑区别
  • if / elif / else 到底什么时候该写,什么时候可以不写
  • raise 的真正作用
  • try / except 这种开发里特别常见的异常处理写法到底是什么

这篇文章我就按自己的学习过程,把这些内容完整整理一遍,既当复盘,也当记录。


一、从 BankAccount 类开始

一开始这个练习要求很简单:

  • 属性:account_number(账号)、balance(余额)
  • 方法:deposit(amount)withdraw(amount)get_balance()

我最开始写的时候,其实连 __init__ 都写得不太稳,比如会写成变量名拼错,或者把传进来的参数和实例属性搞混。

后来我把这个类整理成了一个更清晰的版本:

class BankAccount:
    def __init__(self, account_number, balance=0):
        self._account_number = account_number
        self._balance = balance

    @property
    def account_number(self):
        return self._account_number

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("存款金额必须大于 0")
        self._balance += amount

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("取款金额必须大于 0")
        if amount > self._balance:
            raise ValueError("余额不足")
        self._balance -= amount

    def get_balance(self):
        return self._balance

这个版本虽然不复杂,但它已经把很多核心知识点都包含进去了。


二、Python 里“只读不可改”到底怎么做

我当时有一个很直接的问题:

Python 里怎么保证变量只可读不可改?

后来我明白了,Python 里没有那种绝对意义上的“完全私有、完全不可改”,但可以通过 属性封装 的方式,让外部“只能读,不能直接改”。

最常见的写法就是 @property

例如:

class BankAccount:
    def __init__(self, account_number, balance=0):
        self._account_number = account_number
        self._balance = balance

    @property
    def account_number(self):
        return self._account_number

这样我就可以:

account = BankAccount("10001", 1000)
print(account.account_number)

但如果我直接写:

account.account_number = "99999"

就会报错,因为我只写了 getter,没有写 setter。

这里我顺便也搞清楚了几个命名方式的区别:

1. _x

这是一个约定俗成的“内部变量”写法,意思是:

这个属性你最好别在外部直接来改。

但它只是约定,不是强制。

2. __x

这是名称改写(name mangling),它不是“真正私有”,只是防止你在外部直接误访问。
例如 self.__x 实际会被改成类似 _类名__x 这样的名字。

3. @property

这个才是更常用于“只读控制”的方式。它允许我把一个方法伪装成属性,然后只提供读取,不提供设置。

所以从实际开发角度看,想做“只读属性”,更推荐 @property


三、我一开始把 deposit() 写错了

这个地方是我这次学习里印象特别深的一个点。

我一开始写的是:

def deposit(self, amount):
    if amount <= 0:
        print("存入金额不能小于0!")
    else:
        self._balance -= amount

后来我才意识到,这里最关键的问题不是语法,而是 业务逻辑写反了

deposit 是“存钱”,那余额应该增加,也就是:

self._balance += amount

而不是:

self._balance -= amount

-= 其实是取钱的逻辑,不是存钱的逻辑。

这个问题让我真正开始注意到:

  • deposit():余额增加
  • withdraw():余额减少

以后写业务代码,不能只看语法通不通,更要看这个操作本身的业务含义对不对。


四、print()raise 的区别,我终于弄明白了

一开始我遇到非法金额时,习惯写:

if amount <= 0:
    print("存款金额不能小于 0")

表面上看,这样也“提示错误”了,但后来我明白,print() 和真正的报错不是一回事

print() 是什么

它只是把一句话输出出来,程序本身并不会把它当成错误处理。

比如:

def deposit(self, amount):
    if amount <= 0:
        print("存款金额不能小于 0")
        return
    self._balance += amount

这只是告诉人“这里有问题”,但外部程序很难优雅地知道这次调用失败了。

raise 是什么

raise 才是主动抛出异常。

例如:

def deposit(self, amount):
    if amount <= 0:
        raise ValueError("存款金额必须大于 0")
    self._balance += amount

这表示:

  • 遇到非法输入
  • 当前函数立刻停止
  • 把错误抛给外部
  • 外部必须决定怎么处理

这是一种真正的“程序级错误处理”。

所以我现在的理解是:

  • print():只是显示信息
  • raise:才是真正抛出错误

在正式开发里,明显更推荐 raise


五、为什么有时候不写 else

这也是我这次彻底搞懂的一个点。

比如下面这两种写法:

else

def deposit(self, amount):
    if amount <= 0:
        raise ValueError("存款金额必须大于 0")
    else:
        self._balance += amount

不写 else

def deposit(self, amount):
    if amount <= 0:
        raise ValueError("存款金额必须大于 0")
    self._balance += amount

它们在这个场景里效果是一样的。

原因就在于:
raise 一旦执行,当前函数会立刻结束。

所以如果金额不合法,函数已经结束了,后面的 self._balance += amount 根本不会执行。既然如此,else 就变得有点多余。

我总结出来的一个判断方法

如果 if 里面已经有这些语句之一:

  • raise
  • return
  • break
  • continue

那么很多时候 else 都可以省掉。

因为这个分支已经结束流程了,后面直接写正常逻辑就行。

那什么时候适合写 else 呢?

当我真的是在表达“两个互斥分支”时,else 就很自然。

例如:

if age >= 18:
    print("成年人")
else:
    print("未成年人")

这种就很适合写 else

所以我现在对 else 的理解是:

  • 如果是二选一、两边都要处理,写 else
  • 如果前面已经 raise/return 了,通常可以不写 else

六、elif 到底是什么

我以前脑子里总是把它理解成“else if”,后来才真正记住:

Python 里没有 else if 这个关键字,只有:

elif

它的作用是多分支判断。

例如:

score = 75

if score >= 90:
    print("优秀")
elif score >= 80:
    print("良好")
elif score >= 60:
    print("及格")
else:
    print("不及格")

执行顺序是从上到下,一旦命中某个条件,后面的分支就不再判断。

我现在把这套结构理解成:

  • if:先判断第一个条件
  • elif:前面都不满足,再判断这个
  • else:前面都不满足,最后兜底

七、这次我系统理解了 raise

以前我只知道 raise ValueError(...) 这种写法,现在才明白,raise 不是只配合 ValueError 用,它是一个通用的“抛出异常”机制。

基本写法是:

raise 异常类型("错误信息")

例如:

raise ValueError("值不合法")
raise TypeError("类型不对")
raise ZeroDivisionError("除数不能为0")
raise RuntimeError("当前状态异常")
raise NotImplementedError("这个方法还没有实现")

我目前理解最深的几个异常类型

1. ValueError

类型对,但是值不合法。

比如金额是数字,但小于等于 0。

2. TypeError

类型就不对。

比如本来应该传数字,结果传了字符串。

3. RuntimeError

运行状态不允许。

例如账户被冻结,当前不能取钱。

4. NotImplementedError

这个方法只是接口,子类必须自己实现。

这个在面向对象里特别常见。


八、自定义异常让我感觉“更像开发了”

除了 Python 内置异常,我也学到了可以自己定义异常类。

例如:

class InsufficientFundsError(Exception):
    pass

然后:

def withdraw(self, amount):
    if amount > self._balance:
        raise InsufficientFundsError("余额不足")

这样做的好处是,错误语义更明确。

如果一律都写成 ValueError,虽然也能跑,但不够“业务化”。
而自定义异常可以把业务逻辑表达得更清楚。

比如以后完全可以继续拆:

  • InsufficientFundsError:余额不足
  • AccountFrozenError:账户冻结
  • InvalidTransferAmountError:转账金额非法

这让我第一次感觉到,异常不仅仅是“报错”,还是一种设计代码结构的方式。


九、我终于搞懂了 try / except

这个语法我之前在很多代码里都见过,但总有点似懂非懂。
这次结合银行账户取款,我终于真正理解了。

例如:

try:
    account.withdraw(2000)
except ValueError as e:
    print(e)

它的意思是:

  1. 先尝试执行 account.withdraw(2000)
  2. 如果执行过程中抛出了 ValueError
  3. 那就不要让程序直接崩掉
  4. 把这个异常对象保存到 e
  5. 然后执行 print(e)

所以如果 withdraw() 里写了:

if amount > self._balance:
    raise ValueError("余额不足!")

那么外面就会输出:

余额不足!

as e 是什么意思?

e 就是接住的那个异常对象。

所以:

except ValueError as e:
    print(e)

本质就是把错误对象拿过来,然后打印它里面的提示信息。


十、为什么开发里特别常用 try / except

因为真实程序里有很多错误是“可预期的”。

比如:

  • 用户输入不合法
  • 文件不存在
  • 网络请求失败
  • 字典没有这个键
  • 除数不能为 0
  • 余额不足
  • 账号被冻结

这类情况不是程序彻底坏了,而是程序运行中可能遇到的一种“异常情况”。

所以正常的做法就是:

  • 函数内部负责发现问题并 raise
  • 外部负责决定怎么处理,用 try / except

这是一种职责分离。

内部负责抛

def withdraw(self, amount):
    if amount <= 0:
        raise ValueError("取款金额必须大于0!")
    if amount > self._balance:
        raise ValueError("余额不足!")
    self._balance -= amount

外部负责接

try:
    account.withdraw(2000)
except ValueError as e:
    print("取款失败:", e)

这种模式在开发里真的特别常见。


十一、try / except / else / finally 我也顺带捋清了

异常处理不只是 try / except,还常常和 elsefinally 一起用。

1. else

表示:没有异常时执行

try:
    left_money = account.withdraw(300)
except ValueError as e:
    print("取款失败:", e)
else:
    print("取款成功,余额:", left_money)

2. finally

表示:无论有没有异常都执行

try:
    account.withdraw(300)
except ValueError as e:
    print("取款失败:", e)
finally:
    print("本次操作结束")

这让我明白了一点:

  • except:失败时执行
  • else:成功时执行
  • finally:无论成功失败都执行

在文件处理、数据库连接、网络请求这些场景里,finally 特别重要,因为它常用于释放资源、关闭连接、收尾清理。


十二、我现在对异常处理的整体理解

这次学习下来,我对异常处理已经不再只是“会抄写语法”了,而是能把它串成一个完整流程:

1. 先在函数里做参数校验和状态校验

比如金额是否合法,余额是否足够。

2. 如果发现问题,就 raise

把异常主动抛出去,不在函数内部随便 print 了事。

3. 在外部用 try / except 处理

决定是打印提示、记录日志,还是终止当前操作。

这比以前“出错就直接打印一句话”清晰太多了。


十三、我这次最大的几个收获

如果让我用几句话总结这次学习,我会这样说:

1. 不要只盯着语法,要看业务逻辑

deposit()withdraw() 最容易写反,关键是要理解“存钱加、取钱减”。

2. @property 是控制只读属性的常用方法

它比单纯依赖 _x__x 更适合做封装。

3. raise 才是真正的错误处理

print() 只是输出一句话,不等于异常处理。

4. if 里如果已经 raise/return 了,很多时候就不需要 else

这样代码更清晰,层次更浅。

5. try / except 是开发里非常核心的写法

它不是“高级语法”,而是日常开发经常都要用到的东西。


十四、最后给自己留一个完整示例

为了以后复习方便,我把这次学到的内容整理成一个完整的小例子:

class InsufficientFundsError(Exception):
    pass


class BankAccount:
    def __init__(self, account_number, balance=0):
        self._account_number = account_number
        self._balance = balance

    @property
    def account_number(self):
        return self._account_number

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("存款金额必须大于 0")
        self._balance += amount

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("取款金额必须大于 0")
        if amount > self._balance:
            raise InsufficientFundsError("余额不足")
        self._balance -= amount

    def get_balance(self):
        return self._balance


account = BankAccount("10001", 1000)

try:
    account.deposit(500)
    account.withdraw(2000)
except ValueError as e:
    print("参数错误:", e)
except InsufficientFundsError as e:
    print("取款失败:", e)
else:
    print("操作成功,当前余额:", account.get_balance())
finally:
    print("本次操作结束")

这个例子虽然简单,但它已经把我这次学到的很多知识点都串起来了:

  • 类和对象
  • 只读属性
  • deposit / withdraw
  • raise
  • 自定义异常
  • try / except / else / finally

十五、写在最后

这次看似只是写了一个简单的 BankAccount 类,但我感觉自己真正跨过去的,不只是一个练习题,而是对 Python 控制流程和异常处理的一次系统理解。

以前我写代码时,很多地方只是“照着感觉写”:

  • 该不该用 else
  • 为什么要 raise
  • try / except 到底是在干什么
  • ValueError 和自定义异常有什么差别

现在这些点终于慢慢连起来了。

我越来越觉得,编程里很多东西,第一次看只是“知道”,只有真正拿一个小例子不断追问、不断拆开,最后才能变成“理解”。

而这次的 BankAccount,刚好就是这样一个让我把很多细节都捋顺的小练习。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐