Skip to content

Python 错误处理:程序的"应急预案"

引言:从"硬撑"到"优雅处理"

想象你开车:

  • 硬撑(错误码):仪表盘亮红灯,你猜可能是发动机问题、可能是轮胎问题、可能是没油了——每开一段就要停车检查,效率极低;
  • 应急预案(try...except):车一报警,系统自动识别故障类型,告诉你"左后轮爆胎",同时启动备用方案——问题定位精准,处理及时,车还能继续开

Python 的 try...except...finally 就是程序的"应急预案系统"。


一、为什么不用错误码?

1.1 错误码的困境

python
def foo():
    r = some_function()
    if r == -1:        # 错误码混在返回值里
        return -1      # 一级级往上抛
    return r

def bar():
    r = foo()
    if r == -1:
        print('Error')
    else:
        pass

问题

  • 正常结果和错误码混在一起,调用者必须写大量 if 判断;
  • 出错要一级级上报,代码臃肿;
  • 不同错误用不同数字,记不住、易混淆。

1.2 异常机制的优势

python
def foo():
    return some_function()   # 出错自动抛异常,不用管

def bar():
    foo()                    # 也不用管

def main():
    try:
        bar()
    except SomeError as e:   # 在合适的地方统一处理
        print('出错了:', e)

优势:错误自动冒泡,在能处理的层级捕获,代码简洁。


二、try...except...finally:三段式应急处理

2.1 基本结构

python
try:
    print('try...')
    r = 10 / 0           # 这里会出错
    print('result:', r)  # 不会执行
except ZeroDivisionError as e:
    print('except:', e)  # 处理错误
finally:
    print('finally...')  # 无论是否出错,都会执行
print('END')

输出

try...
except: division by zero
finally...
END

生活化理解

  • try:正常开车上路;
  • except:爆胎了,换备胎;
  • finally:无论是否爆胎,到目的地后都要洗车(必须做的事)。

2.2 没有错误时

python
try:
    r = 10 / 2           # 正常
except ZeroDivisionError as e:
    print('except:', e)  # 不执行
else:
    print('no error!')   # 执行
finally:
    print('finally...')  # 执行

else 的作用:没有异常时执行,与 except 互斥。

2.3 捕获多种错误

python
try:
    r = 10 / int('a')    # int('a') 会抛 ValueError
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
finally:
    print('finally...')

注意顺序:子类异常要在父类之前捕获,否则会被父类"截胡":

python
try:
    foo()
except ValueError as e:      # UnicodeError 是 ValueError 的子类
    print('ValueError')
except UnicodeError as e:    # ❌ 永远执行不到!
    print('UnicodeError')

修正:子类在前,父类在后:

python
except UnicodeError as e:    # ✅ 先捕获子类
    print('UnicodeError')
except ValueError as e:      # 再捕获父类
    print('ValueError')

2.4 跨层级捕获

python
def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')             # foo 出错,bar 不处理,main 统一捕获
    except Exception as e:
        print('Error:', e)

main()

核心思想不需要每层都 try,在能处理的层级捕获即可


三、调用栈:错误的"行车记录仪"

3.1 错误信息长什么样?

python
# err.py
def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    bar('0')

main()

运行:

Traceback (most recent call last):
  File "err.py", line 11, in <module>
    main()
  File "err.py", line 9, in main
    bar('0')
  File "err.py", line 6, in bar
    return foo(s) * 2
  File "err.py", line 3, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero

3.2 如何解读?

从下往上读(从最近调用到源头):

层级文件行号代码分析
最顶层err.py11main()程序入口
err.py9bar('0')main 调 bar
err.py6foo(s) * 2bar 调 foo
源头err.py310 / int(s)错误发生在这里

最后一行 ZeroDivisionError: division by zero错误类型和原因

生活化理解:调用栈像快递物流信息——从发货地到收货地,每个中转站都有记录,最后一行是"包裹破损"的原因。

3.3 核心原则

出错不可怕,可怕的是不知道哪里出错。一定要分析调用栈,定位错误源头。


四、记录错误:程序的"黑匣子"

4.1 logging 记录错误

python
import logging

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        logging.exception(e)   # 记录完整错误堆栈

main()
print('END')   # 程序继续执行!

输出

ERROR:root:division by zero
Traceback (most recent call last):
  File "err.py", line 13, in main
    bar('0')
  ...
ZeroDivisionError: division by zero
END

优势

  • 错误被记录,但程序不中断
  • 可以配置输出到文件,方便事后排查;
  • print 更专业,支持级别(DEBUG/INFO/WARNING/ERROR)。

五、抛出错误:主动报告问题

5.1 为什么要主动抛异常?

python
def foo(s):
    n = int(s)
    if n == 0:
        raise ValueError('invalid value: %s' % s)   # 主动报告问题
    return 10 / n

场景:参数不合法、业务规则违反、外部服务不可用——提前暴露问题,而不是让错误蔓延

5.2 自定义错误类型

python
class FooError(ValueError):
    pass

def foo(s):
    n = int(s)
    if n == 0:
        raise FooError('invalid value: %s' % s)
    return 10 / n

原则:优先用内置类型(ValueErrorTypeError),不够用时才自定义。

5.3 raise 的两种用法

用法 1:原样抛出(记录后继续上报)

python
def bar():
    try:
        foo('0')
    except ValueError as e:
        print('ValueError!')   # 记录一下
        raise                  # 原样抛给上层处理

用法 2:转换错误类型

python
try:
    10 / 0
except ZeroDivisionError:
    raise ValueError('input error!')   # 转换成更合适的类型

生活化理解:员工遇到客户投诉(ZeroDivisionError),记录后上报给经理(raise),经理根据情况转成"服务流程问题"(ValueError)给 CEO——转换要合理,不能把"产品质量"说成"物流延迟"


六、知识链条:错误处理完整流程

代码执行出错

自动抛出异常(或主动 raise)

异常沿调用栈向上冒泡

某层级 try...except 捕获

处理(记录日志/转换类型/恢复操作)

finally 执行清理工作

程序继续运行或优雅退出

七、常见误区与避坑指南

7.1 误区一:捕获所有异常却不处理

python
try:
    risky_operation()
except:              # ❌ 捕获所有,包括 KeyboardInterrupt
    pass             # ❌ 静默吞掉,问题被掩盖

修正:明确捕获类型,或至少记录:

python
except SpecificError as e:
    logging.error(e)
    raise

7.2 误区二:在 finally 里写 return

python
def foo():
    try:
        return 1
    finally:
        return 2     # ❌ 覆盖 try 里的 return!

finally 里的 return 会覆盖前面的返回值,慎用。

7.3 误区三:异常处理嵌套过深

python
try:
    try:
        try:
            ...
        except A:
            ...
    except B:
        ...
except C:
    ...

修正:扁平化,在统一层级处理。

7.4 误区四:用异常控制正常流程

python
# ❌ 用异常代替 if-else
try:
    value = my_dict['key']
except KeyError:
    value = 'default'

# ✅ 更好的方式
value = my_dict.get('key', 'default')

原则:异常用于意外情况,不是常规逻辑


八、实际应用案例

案例 1:文件下载器

python
import logging
import requests

logging.basicConfig(level=logging.ERROR)

def download_file(url, save_path):
    """下载文件,带错误处理和重试"""
    max_retries = 3

    for attempt in range(max_retries):
        try:
            print(f'尝试下载 (第 {attempt + 1} 次)...')
            response = requests.get(url, timeout=10)
            response.raise_for_status()   # 4xx/5xx 抛异常

            with open(save_path, 'wb') as f:
                f.write(response.content)

            print(f'下载成功: {save_path}')
            return True

        except requests.exceptions.Timeout:
            print('下载超时')
            if attempt == max_retries - 1:
                logging.error(f'下载失败,超时: {url}')
                return False

        except requests.exceptions.HTTPError as e:
            print(f'HTTP 错误: {e.response.status_code}')
            logging.error(f'下载失败,HTTP {e.response.status_code}: {url}')
            return False   # 客户端错误不重试

        except Exception as e:
            logging.exception(f'未知错误: {url}')
            return False

# 使用
download_file('https://example.com/file.zip', 'file.zip')
download_file('https://example.com/not-found', 'not-found.zip')

生活化理解:下载器像快递员——超时(收件人不在家)就再试一次,地址错误(404)就直接上报,交通意外(未知错误)就记录后放弃。

案例 2:用户注册验证

python
class ValidationError(ValueError):
    """验证错误基类"""
    pass

class UsernameTooShortError(ValidationError):
    pass

class PasswordTooWeakError(ValidationError):
    pass

class DuplicateUserError(ValidationError):
    pass

def validate_username(username):
    if len(username) < 3:
        raise UsernameTooShortError('用户名至少3个字符')

def validate_password(password):
    if len(password) < 6:
        raise PasswordTooWeakError('密码至少6位')
    if password.isdigit():
        raise PasswordTooWeakError('密码不能全是数字')

def check_duplicate(username):
    # 模拟数据库查询
    existing_users = ['admin', 'root', 'test']
    if username in existing_users:
        raise DuplicateUserError(f'用户名 {username} 已被占用')

def register(username, password):
    """用户注册"""
    try:
        validate_username(username)
        validate_password(password)
        check_duplicate(username)

        # 保存到数据库...
        print(f'注册成功: {username}')
        return True

    except ValidationError as e:
        print(f'注册失败: {e}')
        return False

# 测试
register('ab', '123456')           # 注册失败: 用户名至少3个字符
register('alice', '123')           # 注册失败: 密码至少6位
register('admin', 'abc123')        # 注册失败: 用户名 admin 已被占用
register('bob', 'abc123')          # 注册成功: bob

生活化理解:注册像机场安检——每道关卡(用户名、密码、重复检查)独立,任何一道不通过就明确告知原因,不让问题混过去。


九、实战练习

练习:修复代码错误

运行下面的代码,根据异常信息定位并修复错误:

python
from functools import reduce

def str2num(s):
    return int(s)

def calc(exp):
    ss = exp.split('+')
    ns = map(str2num, ss)
    return reduce(lambda acc, x: acc + x, ns)

def main():
    r = calc('100 + 200 + 345')
    print('100 + 200 + 345 =', r)
    r = calc('99 + 88 + 7.6')
    print('99 + 88 + 7.6 =', r)

main()
参考答案

错误分析'99 + 88 + 7.6''7.6' 是浮点数,int('7.6') 会抛 ValueError

修复str2num 支持浮点数:

python
from functools import reduce

def str2num(s):
    s = s.strip()           # 去除空格
    if '.' in s:
        return float(s)
    return int(s)

def calc(exp):
    ss = exp.split('+')
    ns = map(str2num, ss)
    return reduce(lambda acc, x: acc + x, ns)

def main():
    r = calc('100 + 200 + 345')
    print('100 + 200 + 345 =', r)
    r = calc('99 + 88 + 7.6')
    print('99 + 88 + 7.6 =', r)

main()

十、小结

  1. 错误码 vs 异常:错误码混在返回值里,异常自动冒泡、精准定位;
  2. 三段式try 执行代码,except 处理错误,finally 必须清理;
  3. 捕获顺序:子类在前,父类在后,避免被"截胡";
  4. 调用栈:从下往上读,最后一行是错误源头,分析调用栈是定位错误的关键
  5. 记录错误logging.exception(e) 记录堆栈,程序继续运行;
  6. 抛出错误raise 主动报告问题,可原样抛出或转换类型;
  7. 使用原则:异常用于意外情况,不是常规流程控制;在能处理的层级捕获,不要每层都 try。

错误处理是程序的"应急预案"——平时感觉不到它的存在,一旦出事,它能救命。