Skip to content
话题
Python
Python Try Except:如何正确处理异常

Python Try Except:如何正确处理异常

Updated on

你的 Python 脚本读取一个文件,解析数据,然后将其发送到 API。在你的电脑上它运行得完美无缺。然后它在一台服务器上运行,那里的文件路径是错误的,JSON 格式不正确,或者网络断开了。程序崩溃并输出回溯信息,整个管道停止。这就是 try/except 要解决的问题。与其让错误终止你的程序,不如捕获它们、处理它们,然后继续运行。

📚

Python 中的异常是什么?

异常是一个打断程序正常流程的事件。当 Python 遇到无法执行的操作时——除以零、访问不存在的字典键、打开一个不存在的文件——它会创建一个异常对象并停止执行。如果没有任何东西捕获该异常,程序就会终止并打印回溯信息。

# This crashes the program
result = 10 / 0

输出:

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    result = 10 / 0
ZeroDivisionError: division by zero

异常与语法错误不同。语法错误意味着 Python 根本无法解析你的代码。异常发生在执行期间,即代码已经被成功解析之后。

基本的 Try/Except 语法

try/except 代码块让你尝试可能失败的代码,并定义失败时该怎么做。

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero.")

输出:

Cannot divide by zero.

程序不会崩溃。Python 执行 try 中的代码。当 ZeroDivisionError 发生时,执行跳转到 except 代码块。try/except 之后的所有代码都正常继续。

你也可以捕获异常对象来查看错误消息:

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Error: {e}")

输出:

Error: division by zero

捕获特定异常

Python 有数十种内置异常类型。捕获正确的类型使你的代码更精确。以下是你最常处理的异常。

ValueError

当函数接收到类型正确但值不合适的参数时引发。

try:
    number = int("not_a_number")
except ValueError as e:
    print(f"Invalid value: {e}")

输出:

Invalid value: invalid literal for int() with base 10: 'not_a_number'

TypeError

当操作应用于错误类型的对象时引发。

try:
    result = "hello" + 42
except TypeError as e:
    print(f"Type error: {e}")

输出:

Type error: can only concatenate str (not "int") to str

FileNotFoundError

当你尝试打开一个不存在的文件时引发。

try:
    with open("nonexistent_file.txt", "r") as f:
        content = f.read()
except FileNotFoundError:
    print("File not found. Check the file path.")

KeyError

当你访问一个不存在的字典键时引发。

data = {"name": "Alice", "age": 30}
 
try:
    email = data["email"]
except KeyError as e:
    print(f"Missing key: {e}")

输出:

Missing key: 'email'

IndexError

当你访问超出范围的列表索引时引发。

items = [10, 20, 30]
 
try:
    value = items[5]
except IndexError:
    print("Index out of range.")

多个 Except 代码块

你可以用不同的 except 代码块处理不同的异常类型。Python 按顺序检查并执行第一个匹配的。

def parse_config(raw_value):
    try:
        parts = raw_value.split(":")
        key = parts[0]
        value = int(parts[1])
        return {key: value}
    except IndexError:
        print("Config format error: missing colon separator.")
    except ValueError:
        print("Config format error: value is not a number.")
    except AttributeError:
        print("Config format error: input is not a string.")
 
parse_config("timeout:30")    # Returns {'timeout': 30}
parse_config("timeout")       # Config format error: missing colon separator.
parse_config("timeout:abc")   # Config format error: value is not a number.
parse_config(12345)            # Config format error: input is not a string.

你也可以使用元组在一个 except 代码块中捕获多个异常:

try:
    value = int(input("Enter a number: "))
    result = 100 / value
except (ValueError, ZeroDivisionError) as e:
    print(f"Invalid input: {e}")

Else 代码块

else 代码块仅在 try 代码块中没有发生异常时执行。它将"正常路径"代码与错误处理代码分开。

try:
    number = int("42")
except ValueError:
    print("That is not a valid number.")
else:
    print(f"Successfully parsed: {number}")

输出:

Successfully parsed: 42

else 代码块之所以有用,是因为其中引发的任何异常都不会被前面的 except 代码块捕获。这防止了意外地隐藏成功路径代码中的 bug。

filename = "data.txt"
 
try:
    f = open(filename, "r")
except FileNotFoundError:
    print(f"File '{filename}' does not exist.")
else:
    content = f.read()
    f.close()
    print(f"Read {len(content)} characters.")

如果 open() 失败,except 代码块会处理。如果 open() 成功,else 代码块读取文件。f.read() 期间的任何错误不会在这里被捕获——它会向上传播,这是正确的行为。

Finally 代码块

finally 代码块总是会执行,无论是否发生异常。它是放置清理代码的正确位置:关闭文件、释放锁、断开数据库连接。

f = None
try:
    f = open("data.txt", "r")
    content = f.read()
except FileNotFoundError:
    print("File not found.")
finally:
    if f is not None:
        f.close()
        print("File handle closed.")

即使 try 代码块返回一个值或引发一个未处理的异常,finally 代码块也会执行。

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None
    finally:
        print("Division attempted.")
 
result = divide(10, 3)
# Output: Division attempted.
# result = 3.3333333333333335
 
result = divide(10, 0)
# Output: Division attempted.
# result = None

完整的 Try/Except/Else/Finally 模式

以下是四个代码块如何协同工作:

import json
 
def load_config(filepath):
    """Load and parse a JSON config file."""
    f = None
    try:
        f = open(filepath, "r")
        data = json.load(f)
    except FileNotFoundError:
        print(f"Config file '{filepath}' not found.")
        return {}
    except json.JSONDecodeError as e:
        print(f"Invalid JSON in '{filepath}': {e}")
        return {}
    else:
        print(f"Config loaded successfully with {len(data)} keys.")
        return data
    finally:
        if f is not None:
            f.close()
            print("File handle released.")
 
config = load_config("settings.json")
代码块何时执行用途
try总是(最先)包含可能引发异常的代码
except仅当匹配的异常发生时处理错误
else仅当 try 中没有异常发生时执行成功路径逻辑
finally总是(最后)无论如何都必须运行的清理代码

常见内置异常参考

Python 包含一个内置异常的层次结构。以下是你最常遇到的那些。

异常何时发生示例
Exception大多数异常的基类所有非系统退出异常的父类
ValueError类型正确但值不合适int("abc")
TypeError操作的类型不正确"text" + 5
KeyError字典键不存在d["missing"]
IndexError列表索引超出范围[1,2,3][10]
FileNotFoundError文件不存在open("no.txt")
ZeroDivisionError除以零或取模零1 / 0
AttributeError对象没有该属性None.append(1)
ImportError模块导入失败import nonexistent
OSError操作系统级操作失败磁盘满、权限被拒绝
StopIteration迭代器没有更多元素在耗尽的迭代器上调用 next()
RuntimeError通用运行时错误各种故障的统称

所有这些都继承自 BaseException。在实践中,你应该捕获 Exception 或其子类,永远不要直接捕获 BaseException(它包含 SystemExitKeyboardInterrupt)。

使用 raise 引发异常

当你的代码检测到无效条件时,可以显式引发异常。这是你强制前置条件并向调用者发出错误信号的方式。

def set_age(age):
    if not isinstance(age, int):
        raise TypeError(f"Age must be an integer, got {type(age).__name__}")
    if age < 0 or age > 150:
        raise ValueError(f"Age must be between 0 and 150, got {age}")
    return age
 
# Valid usage
print(set_age(25))  # 25
 
# Invalid usage
try:
    set_age(-5)
except ValueError as e:
    print(e)  # Age must be between 0 and 150, got -5
 
try:
    set_age("thirty")
except TypeError as e:
    print(e)  # Age must be an integer, got str

你也可以在 except 代码块内使用不带参数的 raise 重新引发当前异常。当你想记录错误然后让它继续传播时,这非常有用。

import logging
 
try:
    result = 10 / 0
except ZeroDivisionError:
    logging.error("Division by zero encountered")
    raise  # re-raises the original ZeroDivisionError

自定义异常类

对于较大的项目,定义你自己的异常类。自定义异常使错误处理更具可读性,并允许调用者捕获特定的故障模式。

class ValidationError(Exception):
    """Raised when input data fails validation."""
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"Validation failed on '{field}': {message}")
 
class DatabaseConnectionError(Exception):
    """Raised when the database connection fails."""
    pass
 
# Usage
def validate_email(email):
    if "@" not in email:
        raise ValidationError("email", "Must contain @ symbol")
    if "." not in email.split("@")[1]:
        raise ValidationError("email", "Domain must contain a dot")
    return email
 
try:
    validate_email("userexample.com")
except ValidationError as e:
    print(e)          # Validation failed on 'email': Must contain @ symbol
    print(e.field)    # email
    print(e.message)  # Must contain @ symbol

自定义异常应该继承 Exception,而不是 BaseException。将相关的异常分组在一个共同的基类下,以便调用者可以捕获广泛或狭窄的类别:

class AppError(Exception):
    """Base exception for this application."""
    pass
 
class ConfigError(AppError):
    pass
 
class NetworkError(AppError):
    pass
 
# Caller can catch all app errors or specific ones
try:
    raise NetworkError("Connection timed out")
except AppError as e:
    print(f"Application error: {e}")

Python 异常处理最佳实践

1. 永远不要捕获裸异常

裸露的 except: 会捕获一切,包括 KeyboardInterruptSystemExit。这会隐藏 bug 并使调试变成噩梦。

# BAD - catches everything, hides real bugs
try:
    do_something()
except:
    pass
 
# GOOD - catches specific exceptions
try:
    do_something()
except ValueError as e:
    logging.warning(f"Invalid value: {e}")

如果你必须捕获广泛的范围,请改用 except Exception。这仍然允许 KeyboardInterruptSystemExit 传播。

2. 保持 Try 代码块小巧

只将可能引发异常的代码放在 try 代码块内。大的 try 代码块使得不清楚是哪一行导致了错误。

# BAD - too much code in try
try:
    data = load_data()
    cleaned = clean_data(data)
    result = analyze(cleaned)
    save_results(result)
except Exception as e:
    print(f"Something failed: {e}")
 
# GOOD - narrow try blocks
data = load_data()
cleaned = clean_data(data)
 
try:
    result = analyze(cleaned)
except ValueError as e:
    print(f"Analysis failed: {e}")
    result = default_result()
 
save_results(result)

3. 记录异常,不要静默处理

pass 吞掉异常会产生不可见的 bug。始终记录或报告错误。

import logging
 
try:
    process_record(record)
except ValueError as e:
    logging.error(f"Failed to process record {record['id']}: {e}")

4. 使用具体的异常类型

尽可能捕获最具体的异常类型。这防止意外处理你没有预料到的错误。

方式捕获范围风险等级
except:包括 SystemExit 在内的一切非常高
except Exception:所有标准异常
except ValueError:仅 ValueError
except (ValueError, TypeError):两个特定类型

5. 在 Finally 中清理资源或使用上下文管理器

对于文件句柄、数据库连接和锁,始终使用 finally 或(更好的是)with 语句。

# Prefer context managers for resource cleanup
with open("data.txt", "r") as f:
    content = f.read()
# File is automatically closed, even if an exception occurs

实际应用示例

读取和解析 JSON 文件

import json
 
def read_json_config(filepath):
    """Read a JSON configuration file with proper error handling."""
    try:
        with open(filepath, "r") as f:
            config = json.load(f)
    except FileNotFoundError:
        print(f"Config file not found: {filepath}")
        return None
    except PermissionError:
        print(f"No permission to read: {filepath}")
        return None
    except json.JSONDecodeError as e:
        print(f"Invalid JSON at line {e.lineno}, column {e.colno}: {e.msg}")
        return None
    else:
        print(f"Loaded config with keys: {list(config.keys())}")
        return config
 
config = read_json_config("app_config.json")
if config:
    db_host = config.get("database_host", "localhost")

发起 HTTP API 调用

import urllib.request
import urllib.error
import json
 
def fetch_user(user_id):
    """Fetch user data from an API with retry logic."""
    url = f"https://jsonplaceholder.typicode.com/users/{user_id}"
    max_retries = 3
 
    for attempt in range(1, max_retries + 1):
        try:
            with urllib.request.urlopen(url, timeout=5) as response:
                data = json.loads(response.read().decode())
            return data
        except urllib.error.HTTPError as e:
            if e.code == 404:
                print(f"User {user_id} not found.")
                return None
            print(f"HTTP error {e.code} on attempt {attempt}")
        except urllib.error.URLError as e:
            print(f"Network error on attempt {attempt}: {e.reason}")
        except json.JSONDecodeError:
            print("API returned invalid JSON.")
            return None
 
    print(f"All {max_retries} attempts failed.")
    return None
 
user = fetch_user(1)
if user:
    print(f"Found user: {user['name']}")

处理 CSV 数据

import csv
 
def process_sales_data(filepath):
    """Process a CSV file with robust error handling."""
    results = []
 
    try:
        with open(filepath, "r", newline="") as f:
            reader = csv.DictReader(f)
            for row_num, row in enumerate(reader, start=2):
                try:
                    amount = float(row["amount"])
                    quantity = int(row["quantity"])
                    results.append({
                        "product": row["product"],
                        "total": amount * quantity,
                    })
                except KeyError as e:
                    print(f"Row {row_num}: Missing column {e}")
                except ValueError as e:
                    print(f"Row {row_num}: Invalid number - {e}")
    except FileNotFoundError:
        print(f"File not found: {filepath}")
    except PermissionError:
        print(f"Cannot read file: {filepath}")
 
    return results

Try/Except 与 If/Else:何时使用

Python 遵循 EAFP 原则:"Easier to Ask Forgiveness than Permission"(请求宽恕比请求许可更容易)。这与 LBYL 方法形成对比:"Look Before You Leap"(三思而后行)。

# LBYL (Look Before You Leap) - using if/else
if "email" in user_data:
    email = user_data["email"]
else:
    email = "unknown"
 
# EAFP (Easier to Ask Forgiveness) - using try/except
try:
    email = user_data["email"]
except KeyError:
    email = "unknown"
标准if/else (LBYL)try/except (EAFP)
最佳场景检查廉价且错误常见错误罕见或检查代价高
性能(无错误)稍慢(每次都有额外检查)稍快(无检查开销)
性能(发生错误)相同较慢(创建异常有开销)
竞态条件可能(状态可能在检查和使用之间改变)无(原子操作)
可读性对简单条件清晰对可能以多种方式失败的操作更好
文件操作if os.path.exists(path) -- 文件可能在检查和打开之间被删除try: open(path) -- 处理实际的失败
字典访问if key in dict -- 简单快速try: dict[key] -- 或者直接使用 dict.get(key, default)

使用 try/except 的场景:

  • 失败情况很少(异常针对"无错误"路径进行了优化)。
  • 检查本身与操作一样昂贵(例如,检查文件是否存在然后再打开它)。
  • 一系列操作中可能出现多个问题。
  • 竞态条件是一个问题(特别是涉及文件和网络资源时)。

使用 if/else 的场景:

  • 条件检查廉价且失败常见。
  • 你在处理之前验证用户输入。
  • 逻辑作为条件语句更清晰易读。

使用 RunCell 更快地调试异常

当你在 Jupyter 笔记本中工作时,异常可能会打断你的分析流程。你在 DataFrame 的第 50,000 行遇到一个 KeyError,或者一个 TypeError 在管道的三个单元格深处浮出水面。追踪根本原因意味着滚动查看回溯信息和手动检查变量。

RunCell (opens in a new tab) 是一个直接在 Jupyter 内运行的 AI 代理。它读取完整的回溯信息,检查你当前作用域中的变量,并在上下文中建议修复方案。以下是它如何帮助异常处理:

  • 回溯分析。 RunCell 解析异常链并精确定位哪个变量或操作导致了故障,即使在嵌套函数调用中也是如此。
  • 修复建议。 不需要搜索 Stack Overflow,RunCell 生成一个可以立即运行的修正代码单元。它知道应该添加 try/except、修复类型转换还是处理缺失的键。
  • 预防性检查。 RunCell 可以扫描你的代码并标记可能引发异常的操作——比如不使用 .get() 访问字典键,或不检查零就进行除法——在你运行单元格之前。

由于 RunCell 在你现有的 Jupyter 环境中运行,它可以访问你的实际数据和变量。它提供的建议是针对你的具体情况的,而非通用建议。

常见问题

Python 中 try/except 和 try/catch 有什么区别?

Python 使用 try/except,而不是 try/catchtry/catch 语法属于 Java、JavaScript 和 C++ 等语言。在 Python 中,关键字是 except。功能是相同的:你尝试可能失败的代码并为失败情况定义一个处理程序。

Python 中可以使用多个 except 代码块吗?

可以。你可以链接任意数量的 except 代码块,每个捕获不同的异常类型。Python 按顺序评估并执行第一个匹配的代码块。你也可以使用元组在一个代码块中捕获多个异常:except (ValueError, TypeError) as e:

什么时候应该在 try/except 中使用 else?

当你有只在 try 代码块成功时才应该运行的代码时,使用 else 代码块。主要好处是 else 代码块中引发的异常不会被前面的 except 代码块捕获,防止你意外地静默不相关的错误。

Python 中 finally 总是会执行吗?

是的。finally 代码块无论 try 代码块是正常完成、引发了已处理的异常还是引发了未处理的异常都会执行。即使 tryexcept 代码块包含 return 语句也会执行。唯一的例外是 Python 进程被外部终止或调用了 os._exit()

如何在 Python 中创建自定义异常?

创建一个继承自 Exception 的新类。你可以添加自定义属性并重写 __init__ 方法。例如:class MyError(Exception): pass。对于更复杂的情况,添加错误代码或上下文数据等字段。始终继承 Exception,而不是 BaseException

总结

Python 的 try/except 是处理运行时错误的标准机制。它让你捕获特定异常、运行清理代码,并在出现问题时保持程序稳定。完整的模式——try/except/else/finally——涵盖了每种场景:尝试操作、处理故障、运行成功逻辑和清理资源。

关键原则很简单。捕获具体的异常,而不是宽泛的异常。保持 try 代码块小巧。始终记录或报告错误而不是静默处理。使用 finally 或上下文管理器进行清理。在你自己的代码中引发有意义的异常并附带清晰的错误消息。

无论你是在读取文件、发起 API 调用还是处理用户输入,适当的异常处理是凌晨 2 点崩溃的脚本和记录错误并继续运行的脚本之间的区别。从基础开始——在可能失败的代码周围包裹一个简单的 try/except——然后随着项目的增长构建自定义异常层次结构。

📚