
大部分人都不知道如何在 Python 中自定义异常
先问个问题:你写代码的时候是不是经常遇到这种情况?程序报错了,但就一句干巴巴的 "Exception: error",根本不知道哪儿错了。比如你写个硬件监控程序,温度过高和电压不稳都报同一个错,排查起来头都大了。
这时候自定义异常就派上用场了。简单说,自定义异常就是给自己的程序量身定做错误类型,让错误信息更明白,调试更方便。
自定义异常其实特简单,就一句话:继承 Python 自带的 Exception 类。来看个例子:
# 最简单的自定义异常
class MyFirstError(Exception):
pass
# 用一下试试
try:
raise MyFirstError("这是我自定义的第一个异常")
except MyFirstError as e:
print("捕获到异常:", e)运行这段代码,会输出:捕获到异常: 这是我自定义的第一个异常。就这么简单,你已经创建了一个自己的异常类!
光有个名字不够,咱们还得让异常携带更多信息。比如硬件错误,最好能带上温度、电压这些数据。这时候就需要重写init方法:
# 硬件错误异常,带温度信息
class HardwareError(Exception):
def __init__(self, message, temperature=None):
# 先调用父类的初始化方法,这步千万别忘!
super().__init__(message)
# 增加自定义属性
self.temperature = temperature
# 用起来试试
try:
# 模拟硬件过热
raise HardwareError("CPU温度过高", temperature=137)
except HardwareError as e:
print(f"错误信息:{e}")
print(f"温度值:{e.temperature}℃")运行结果:
错误信息:CPU温度过高
温度值:137℃看到没?这样报错不仅知道错哪儿了,还能拿到关键数据,调试的时候是不是方便多了?
自定义异常也能搞继承,就像儿子继承老子的财产一样。比如硬件错误可以细分出 CPU 错误、内存错误,它们都属于硬件错误,但又有自己的特点。
# 硬件错误基类
class HardwareError(Exception):
def __init__(self, message, device=None):
super().__init__(message)
self.device = device # 出问题的设备
# CPU错误(继承硬件错误)
class CPUError(HardwareError):
def __init__(self, message, device="CPU", temperature=None):
super().__init__(message, device)
self.temperature = temperature # CPU特有:温度
# 内存错误(继承硬件错误)
class MemoryError(HardwareError): # 注意:别和Python自带的MemoryError重名,这里只是举例
def __init__(self, message, device="内存", used=None, total=None):
super().__init__(message, device)
self.used = used # 内存特有:已使用
self.total = total # 内存特有:总容量默认的异常信息有时候太简陋了,比如上面的例子只会显示 "CPU 温度过高",但我们想让它直接显示 "CPU 温度过高:137℃",这时候就得重写str方法。
class HardwareError(Exception):
def __init__(self, message, device=None):
super().__init__(message)
self.device = device
# 重写__str__方法,定制显示格式
def __str__(self):
base_msg = super().__str__() # 先拿到父类的错误信息
if self.device:
return f"【{self.device}错误】{base_msg}"
return base_msg
class CPUError(HardwareError):
def __init__(self, message, device="CPU", temperature=None):
super().__init__(message, device)
self.temperature = temperature
def __str__(self):
base_msg = super().__str__() # 先拿到父类的显示
if self.temperature:
return f"{base_msg},当前温度:{self.temperature}℃"
return base_msg
# 测试一下
try:
raise CPUError("温度超过阈值", temperature=137)
except CPUError as e:
print(e) # 会显示:【CPU错误】温度超过阈值,当前温度:137℃是不是清晰多了?团队协作的时候,别人看到这样的错误信息,一眼就知道问题出在哪儿。
异常类型 | 默认显示 | 自定义str后 |
|---|---|---|
普通 Exception | Exception: 出错了 | 出错了 |
自定义 HardwareError | HardwareError: 连接失败 | 【未知设备错误】连接失败 |
自定义 CPUError | CPUError: 温度过高 | 【CPU 错误】温度过高,当前温度:137℃ |
有时候你需要把异常对象存起来(比如用 pickle 序列化),但自定义异常默认可能搞不定,这时候就得用reduce方法。
import pickle
class CPUError(Exception):
def __init__(self, message, temperature):
super().__init__(message)
self.temperature = temperature
# 创建异常对象
error = CPUError("过热", 137)
# 尝试序列化
try:
data = pickle.dumps(error)
# 反序列化
new_error = pickle.loads(data)
print(new_error.temperature) # 这步可能会报错!
except Exception as e:
print("序列化出错:", e)为啥会出错?因为 pickle 在序列化对象时,需要知道怎么保存和恢复对象的状态。自定义异常如果有额外属性,默认的处理可能不够。
import pickle
class CPUError(Exception):
def __init__(self, message, temperature):
super().__init__(message)
self.temperature = temperature
# 自定义序列化方法
def __reduce__(self):
# 返回一个元组:(构造函数, (参数1, 参数2, ...), 状态字典)
return (self.__class__, (self.args[0], self.temperature), {})
# 再试一次
error = CPUError("过热", 137)
data = pickle.dumps(error)
new_error = pickle.loads(data)
print(new_error.temperature) # 输出137,成功了!
print(new_error) # 输出过热reduce方法告诉 pickle:用哪个构造函数,传什么参数,以及需要保存的状态。这样序列化和反序列化就没问题了。
多态是面向对象的重要概念,简单说就是:"同一个操作,作用在不同对象上,产生不同的结果"。在异常处理里,多态非常有用。
# 基类异常
class HardwareError(Exception):
def __init__(self, message):
super().__init__(message)
def handle(self):
"""处理错误的方法,子类要重写"""
raise NotImplementedError("子类必须实现handle方法")
# CPU错误
class CPUError(HardwareError):
def __init__(self, message, temperature):
super().__init__(message)
self.temperature = temperature
def handle(self):
print(f"处理CPU错误:降低负载,当前温度{self.temperature}℃")
# 内存错误
class MemoryError(HardwareError):
def __init__(self, message, used, total):
super().__init__(message)
self.used = used
self.total = total
def handle(self):
print(f"处理内存错误:释放缓存,已用{self.used}/{self.total}")
# 统一处理函数
def handle_error(error):
# 不管是CPUError还是MemoryError,都调用handle方法
error.handle()
# 测试多态
try:
raise CPUError("CPU过热", 137)
except HardwareError as e:
handle_error(e) # 输出:处理CPU错误:降低负载,当前温度137℃
try:
raise MemoryError("内存不足", 80, 100)
except HardwareError as e:
handle_error(e) # 输出:处理内存错误:释放缓存,已用80/100看到没?handle_error 函数根本不用管传入的是 CPUError 还是 MemoryError,只要是 HardwareError 的子类,就直接调用 handle 方法。这就是多态的好处:简化代码,增加扩展性。
class HardwareError(Exception):
def handle(self):
raise NotImplementedError()
class DiskError(HardwareError):
# 忘记重写handle方法
pass
# 调用时会报错
try:
raise DiskError("磁盘满了")
except HardwareError as e:
e.handle() # 会抛出NotImplementedError解决办法:子类一定要重写父类中标记为必须重写的方法。
def bad_handle_error(error):
# 用type判断类型,破坏了多态
if type(error) == CPUError:
error.handle()
elif type(error) == MemoryError:
error.handle()
else:
print("无法处理")
# 问题:如果新增了DiskError,这个函数就处理不了了正确做法:依赖父类接口,不关心具体类型。
try:
raise CPUError("过热")
except HardwareError as e: # 父类异常
print("处理硬件错误")
except CPUError as e: # 子类异常,永远不会被捕获
print("处理CPU错误")这是个大坑!Python 异常捕获是从上到下的,父类异常放在前面,子类异常就永远没机会被捕获了。正确的顺序是:先捕获子类异常,再捕获父类异常。
问:为什么要自定义异常?
答:因为 Python 自带的异常太笼统了。比如一个硬件监控程序,CPU 过热和内存不足是完全不同的问题,用自定义异常能区分它们,让错误信息更具体,调试更方便,代码也更易维护。
问:自定义异常应该继承哪个类?
答:应该继承 Exception 类,而不是 BaseException。因为 BaseException 包括了一些特殊的异常(比如 KeyboardInterrupt),一般我们不希望自己的异常被这些特殊异常的处理逻辑捕获。
问:如何让自定义异常携带更多信息?
答:在init方法里添加属性,比如 CPUError 可以加个 temperature 属性。然后调用 super ().init() 初始化父类的信息,确保基础的错误信息也能保留。
问:pickle 序列化自定义异常时可能遇到什么问题?怎么解决?
答:如果自定义异常有额外的属性,默认序列化可能无法正确保存这些属性,导致反序列化后丢失数据。解决办法是重写reduce方法,告诉 pickle 如何保存和恢复对象的状态。
问:什么是多态?在异常处理中怎么体现?
答:多态就是同一个操作(比如调用 handle 方法),作用在不同对象(CPUError、MemoryError)上,会有不同的结果。在异常处理中,我们可以用父类 HardwareError 接收所有子类异常,然后统一调用 handle 方法,每个子类会按自己的方式处理,这就是多态的体现。
问:使用多态时要注意什么?
答:首先子类要正确重写父类的方法;其次异常捕获要先子类后父类,不然子类异常会被父类捕获;还有不要用 type 判断具体类型,要依赖父类的接口,这样才能保持扩展性。
最后给个综合示例,包含所有知识点:
import pickle
# 基类异常
class HardwareError(Exception):
def __init__(self, message, device):
super().__init__(message)
self.device = device
def __str__(self):
return f"【{self.device}错误】{super().__str__()}"
def handle(self):
raise NotImplementedError("子类必须实现handle方法")
def __reduce__(self):
return (self.__class__, (self.args[0], self.device))
# CPU异常
class CPUError(HardwareError):
def __init__(self, message, temperature):
super().__init__(message, "CPU")
self.temperature = temperature
def __str__(self):
base_str = super().__str__()
return f"{base_str},温度:{self.temperature}℃"
def handle(self):
print(f"处理CPU错误:关闭部分进程,降低温度")
def __reduce__(self):
return (self.__class__, (self.args[0], self.temperature))
# 内存异常
class MemoryError(HardwareError):
def __init__(self, message, used, total):
super().__init__(message, "内存")
self.used = used
self.total = total
def __str__(self):
base_str = super().__str__()
return f"{base_str},已用{self.used}MB/共{self.total}MB"
def handle(self):
print(f"处理内存错误:释放缓存,清理临时文件")
def __reduce__(self):
return (self.__class__, (self.args[0], self.used, self.total))
# 测试函数
def test_errors():
# 测试异常多态
errors = [
CPUError("温度过高", 137),
MemoryError("内存不足", 8000, 10000)
]
for error in errors:
try:
raise error
except HardwareError as e:
print("捕获到错误:", e)
e.handle()
# 测试序列化
data = pickle.dumps(e)
new_e = pickle.loads(data)
print("反序列化后:", new_e)
print("-" * 50)
test_errors()运行这段代码,你会看到:
总结
自定义异常看起来简单,但里面门道不少。用好它能让你的代码更专业,错误处理更高效。多态作为面向对象的核心思想,在异常处理中尤其重要,能让代码更灵活、更易扩展。记住那些常见错误,面试的时候也能应对自如。
最后再强调一句:写代码时多想想 "如果出了这个错,我想知道哪些信息",按这个思路设计的自定义异常,肯定差不了!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。