Python & AI Tutorials Logo
Python 编程

26. 使用异常与验证的防御式编程技巧

防御式编程(defensive programming) 指的是在问题发生之前就预先考虑并应对的代码编写方式。防御式代码不会假设一切都能完美运行,而是会验证输入、优雅地处理错误,并检查前提假设。这种方法能让程序更可靠、更易于调试(debugging),也更不容易意外崩溃。

在前面的章节中,我们学习了如何在异常出现时进行处理。现在我们将学习如何从源头避免许多错误的发生,以及在错误确实发生时如何尽早捕获问题。

26.1) 验证函数参数

函数(function) 经常从程序的其他部分或用户那里接收数据。如果函数收到无效数据,可能会产生错误结果、因令人困惑的错误而崩溃,或在程序的其他地方引发问题。参数验证(argument validation) 指的是在使用参数之前,先检查函数参数是否满足你的要求。

26.1.1) 为什么要验证参数?

考虑这个用于计算学生成绩百分比的函数:

python
def calculate_percentage(points_earned, total_points):
    return (points_earned / total_points) * 100
 
# 使用该函数
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%")  # Output: Grade: 85.0%

当输入有效时,它工作正常。但如果数据有问题,会发生什么?

python
# 问题 1:除以零
percentage = calculate_percentage(85, 0)  # ZeroDivisionError!
 
# 问题 2:负值(没有意义)
percentage = calculate_percentage(-10, 100)  # -10.0%
 
# 问题 3:已得分超过总分(不可能)
percentage = calculate_percentage(120, 100)  # 120.0%

没有验证时,函数要么崩溃,要么产生毫无意义的结果。错误信息并不能从业务逻辑角度解释哪里出错——它们只展示了技术层面的失败。

26.1.2) 使用条件语句进行基础参数验证

最简单的验证方法是使用 if 语句检查参数,并在参数无效时抛出异常:

python
def calculate_percentage(points_earned, total_points):
    # 验证 total_points
    if total_points <= 0:
        raise ValueError("total_points must be positive")
    
    # 验证 points_earned
    if points_earned < 0:
        raise ValueError("points_earned cannot be negative")
    
    if points_earned > total_points:
        raise ValueError("points_earned cannot exceed total_points")
    
    # 所有验证都通过——可以安全计算
    return (points_earned / total_points) * 100
 
# 有效用法
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%")  # Output: Grade: 85.0%
 
# 无效用法——清晰的错误信息
try:
    percentage = calculate_percentage(85, 0)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: total_points must be positive
 
try:
    percentage = calculate_percentage(-10, 100)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: points_earned cannot be negative
 
try:
    percentage = calculate_percentage(120, 100)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: points_earned cannot exceed total_points

现在当出现问题时,错误信息会清楚地说明问题是什么以及如何修复。

26.1.3) 验证参数类型

有时你需要确保参数是正确的类型:

python
def calculate_discount(price, discount_percent):
    # 验证类型
    if not isinstance(price, (int, float)):
        raise TypeError("price must be a number")
    
    if not isinstance(discount_percent, (int, float)):
        raise TypeError("discount_percent must be a number")
    
    # 验证取值
    if price < 0:
        raise ValueError("price cannot be negative")
    
    if not (0 <= discount_percent <= 100):
        raise ValueError("discount_percent must be between 0 and 100")
    
    # 计算折扣
    discount_amount = price * (discount_percent / 100)
    return price - discount_amount
 
# 有效用法
final_price = calculate_discount(50.00, 20)
print(f"Final price: ${final_price:.2f}")  # Output: Final price: $40.00
 
# 类型错误
try:
    final_price = calculate_discount("50", 20)
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: price must be a number
 
# 值错误
try:
    final_price = calculate_discount(50.00, 150)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: discount_percent must be between 0 and 100

isinstance() 函数用于检查一个对象是否是指定类型或类型集合的实例。我们传入一个元组 (int, float) 来接受整数或浮点数,因为两者都是价格的有效数值类型。

何时验证类型: Python 的哲学是“鸭子类型(duck typing)”——只要对象表现得像你需要的那样,就使用它。类型验证在以下情况下最有用:

  • 你在编写一个会被他人使用的函数(function)
  • 类型错误会在后面引发令人困惑的失败
  • 该函数是公共 API 或库的一部分

26.1.4) 验证集合参数

当函数(function) 接受列表(list)、字典或其他集合时,要同时验证集合本身及其内容:

python
def calculate_average_grade(grades):
    # 验证集合本身
    if not isinstance(grades, list):
        raise TypeError("grades must be a list")
    
    if len(grades) == 0:
        raise ValueError("grades list cannot be empty")
    
    # 验证集合中的每个成绩
    for i, grade in enumerate(grades):
        if not isinstance(grade, (int, float)):
            raise TypeError(f"grade at index {i} must be a number, got {type(grade).__name__}")
        
        if not (0 <= grade <= 100):
            raise ValueError(f"grade at index {i} must be between 0 and 100, got {grade}")
    
    # 所有验证都通过
    return sum(grades) / len(grades)
 
# 有效用法
grades = [85, 92, 78, 95]
average = calculate_average_grade(grades)
print(f"Average: {average:.1f}")  # Output: Average: 87.5
 
# 空列表错误
try:
    average = calculate_average_grade([])
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: grades list cannot be empty
 
# 成绩类型无效
try:
    average = calculate_average_grade([85, "92", 78])
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: grade at index 1 must be a number, got str
 
# 成绩值无效
try:
    average = calculate_average_grade([85, 92, 150])
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: grade at index 2 must be between 0 and 100, got 150

注意,在验证集合元素时,我们在错误信息中包含了索引。这有助于精确定位到底是哪个条目有问题,尤其是在大型集合中。

类型无效

取值无效

有效

函数被调用

验证
参数

抛出 TypeError

抛出 ValueError

执行函数逻辑

返回结果

调用方处理异常

26.2) 检查用户输入的有效性

用户输入天生不可靠——用户会打错字、误解说明,或以意想不到的格式输入数据。验证用户输入可以防止这些错误导致程序崩溃或产生错误结果。

26.2.1) 基础输入验证模式

输入验证的基本模式是将 input() 与验证检查结合起来:

python
# 获取用户输入
age_str = input("Enter your age: ")
 
# 验证输入
try:
    age = int(age_str)
    if age < 0:
        print("Error: Age cannot be negative")
    elif age > 150:
        print("Error: Age seems unrealistic")
    else:
        print(f"You are {age} years old")
except ValueError:
    print("Error: Please enter a valid number")

这个模式包含三个部分:

  1. 以字符串获取输入
  2. 尝试将其转换为所需类型
  3. 检查转换后的值是否有效

让我们用不同输入看看实际效果:

python
# 有效输入
# User enters: 25
# Output: You are 25 years old
 
# 无效类型
# User enters: twenty-five
# Output: Error: Please enter a valid number
 
# 无效值(负数)
# User enters: -5
# Output: Error: Age cannot be negative
 
# 无效值(不现实)
# User enters: 200
# Output: Error: Age seems unrealistic

26.2.2) 验证输入范围与格式

有些输入必须落在特定范围内,或匹配特定格式:

python
# 验证月份(1-12)
month_str = input("Enter month (1-12): ")
try:
    month = int(month_str)
    if not (1 <= month <= 12):
        print("Error: Month must be between 1 and 12")
    else:
        print(f"Month: {month}")
except ValueError:
    print("Error: Please enter a whole number")
 
# 验证邮箱格式(简单检查)
email = input("Enter email: ")
if '@' not in email or '.' not in email:
    print("Error: Email must contain @ and .")
else:
    print(f"Email: {email}")
 
# 验证 yes/no 输入
response = input("Continue? (yes/no): ").lower().strip()
if response not in ['yes', 'no', 'y', 'n']:
    print("Error: Please answer yes or no")
else:
    if response in ['yes', 'y']:
        print("Continuing...")
    else:
        print("Stopping...")

这里的邮箱验证是刻意保持简单的——它只检查基本结构。真实的邮箱验证要复杂得多,通常会使用正则表达式(regular expressions)(我们会在第 39 章学习)。

26.2.3) 提供有帮助的错误信息

好的错误信息能准确告诉用户哪里错了,以及如何修复:

python
# 糟糕的错误信息
password = input("Enter password: ")
if len(password) < 8:
    print("Error: Invalid password")  # Not helpful!
 
# 更好的错误信息
password = input("Enter password: ")
if len(password) < 8:
    print("Error: Password must be at least 8 characters long")
    print(f"Your password is only {len(password)} characters")
 
# 更好——预先说明所有要求
print("Password requirements:")
print("- At least 8 characters")
print("- Must contain at least one number")
password = input("Enter password: ")
 
# 检查长度
if len(password) < 8:
    print(f"Error: Password too short ({len(password)} characters)")
    print("Password must be at least 8 characters")
# 检查数字
elif not any(char.isdigit() for char in password):
    print("Error: Password must contain at least one number")
else:
    print("Password accepted")

any() 函数会在可迭代对象(iterable) 中任意元素为真时返回 True。这里 char.isdigit() 检查每个字符是否为数字,而 any() 告诉我们是否至少有一个字符通过了测试。

转换失败

转换成功

超出范围

格式无效

有效

获取用户输入

尝试类型转换

ValueError:
格式无效

检查取值
约束

值错误:
清晰消息

格式错误:
清晰消息

使用输入

显示错误,
说明期望格式

26.3) 结合 input()、循环与 try/except 实现健壮的输入处理

单次验证检查很有用,但它无法应对用户反复输入错误的情况。如果用户输入了无效数据,你的程序应该再给他们一次机会。将循环(loop) 与验证结合,就能创建健壮的输入处理:持续询问直到获得有效数据。

26.3.1) 基础输入循环模式

基本模式使用 while 循环,一直持续直到收到有效输入:

python
# 持续询问直到获得有效年龄
while True:
    age_str = input("Enter your age: ")
    try:
        age = int(age_str)
        if age < 0:
            print("Error: Age cannot be negative. Please try again.")
        elif age > 150:
            print("Error: Age seems unrealistic. Please try again.")
        else:
            # 输入有效——跳出循环
            break
    except ValueError:
        print("Error: Please enter a valid number.")
 
print(f"You are {age} years old")

这个模式包含几个关键要素:

  • while True: 创建一个无限循环
  • 验证发生在循环内部
  • 当输入有效时使用 break 退出循环
  • 错误信息鼓励用户再试一次

让我们看看它如何处理各种输入:

python
# 示例交互:
# Enter your age: twenty
# Error: Please enter a valid number.
# Enter your age: -5
# Error: Age cannot be negative. Please try again.
# Enter your age: 25
# You are 25 years old

26.3.2) 创建可复用的输入函数

当你在多个地方都需要同一种经过验证的输入时,可以创建一个函数(function):

python
def get_positive_integer(prompt):
    """Keep asking until user enters a positive integer."""
    while True:
        try:
            value = int(input(prompt))
            if value <= 0:
                print("Error: Please enter a positive number.")
            else:
                return value
        except ValueError:
            print("Error: Please enter a valid whole number.")
 
def get_number_in_range(prompt, min_value, max_value):
    """Keep asking until user enters a number in the specified range."""
    while True:
        try:
            value = float(input(prompt))
            if value < min_value or value > max_value:
                print(f"Error: Please enter a number between {min_value} and {max_value}.")
            else:
                return value
        except ValueError:
            print("Error: Please enter a valid number.")
 
# Using the functions
quantity = get_positive_integer("Enter quantity: ")
print(f"Quantity: {quantity}")
 
grade = get_number_in_range("Enter grade (0-100): ", 0, 100)
print(f"Grade: {grade}")
 
temperature = get_number_in_range("Enter temperature (-50 to 50): ", -50, 50)
print(f"Temperature: {temperature}°C")

这些函数封装了验证逻辑,让你的主代码更简洁、更易读。它们也能确保整个程序中验证行为的一致性。

26.4) 使用断言进行开发期不变量检查

断言(assertions) 是一种特殊检查,用于在开发期间验证代码的假设是正确的。与验证(处理来自用户或外部数据的预期错误)不同,断言用于捕获编程错误——也就是如果代码正确,这些情况就不应该发生。

26.4.1) 断言是什么,以及何时使用

断言(assertion) 是一个在代码某个位置应该始终为真的语句。如果为假,就说明你的程序逻辑存在根本问题:

python
def calculate_average(numbers):
    # 如果函数被正确调用,这种情况不应该发生
    assert len(numbers) > 0, "numbers list cannot be empty"
    
    return sum(numbers) / len(numbers)
 
# 正确用法
grades = [85, 90, 78]
average = calculate_average(grades)
print(f"Average: {average:.1f}")  # Output: Average: 84.3
 
# 错误用法——触发断言
empty_list = []
average = calculate_average(empty_list)  # AssertionError: numbers list cannot be empty

当断言失败时,Python 会抛出一个带有你消息的 AssertionError。这会立即停止程序并向你展示假设在哪里被违反。

关键区别:

  • 验证(validation)(使用 ifraise):用于处理来自用户或外部数据的预期问题
  • 断言(assertions):用于在开发期间捕获编程 bug
python
# 验证——处理预期的用户错误
def get_positive_number(prompt):
    while True:
        try:
            value = float(input(prompt))
            if value <= 0:
                print("Error: Please enter a positive number.")
            else:
                return value
        except ValueError:
            print("Error: Please enter a valid number.")
 
# 断言——捕获编程错误
def calculate_discount(price, discount_rate):
    # 如果程序编写正确,这些条件不应被违反
    assert price >= 0, "price should be non-negative"
    assert 0 <= discount_rate <= 1, "discount_rate should be between 0 and 1"
    
    return price * (1 - discount_rate)

26.4.2) 检查函数前置条件

断言非常适合用来验证函数的前置条件(preconditions)(函数执行前必须为真的要求)是否满足:

python
def get_list_element(items, index):
    """Get an element from a list at the specified index."""
    # 前置条件
    assert isinstance(items, list), "items must be a list"
    assert isinstance(index, int), "index must be an integer"
    assert 0 <= index < len(items), f"index {index} out of range for list of length {len(items)}"
    
    return items[index]
 
# 正确用法
numbers = [10, 20, 30, 40]
value = get_list_element(numbers, 2)
print(f"Value: {value}")  # Output: Value: 30
 
# 编程错误——类型不对
value = get_list_element("not a list", 0)  # AssertionError: items must be a list
 
# 编程错误——索引无效
value = get_list_element(numbers, 10)  # AssertionError: index 10 out of range for list of length 4

这些断言有助于在开发期间捕获 bug。如果你不小心传入了错误类型或无效索引,断言会立即告诉你哪里出了问题。

26.4.3) 检查函数后置条件

后置条件(postconditions) 是函数执行后必须为真的条件。断言可以验证函数产生了有效结果:

python
def calculate_percentage(part, whole):
    """Calculate what percentage 'part' is of 'whole'."""
    # 前置条件
    assert whole > 0, "whole must be positive"
    assert part >= 0, "part must be non-negative"
    
    # 计算百分比
    percentage = (part / whole) * 100
    
    # 后置条件——结果应是有效百分比
    assert 0 <= percentage <= 100, f"percentage {percentage} is outside valid range"
    
    return percentage
 
# 这能正确工作
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")  # Output: Percentage: 25.0%
 
# 这揭示了函数中的逻辑错误
# (我们没有检查 part <= whole)
percentage = calculate_percentage(150, 100)  # AssertionError: percentage 150.0 is outside valid range

后置条件断言捕获了我们函数中的一个 bug——我们忘记验证 part 不会超过 whole。这正是断言的用途:捕获编程错误。

26.4.4) 断言可以被禁用

断言的一个重要特性是:当使用 -O(optimize)标志运行 Python 时,断言可以被禁用:

python
# 该文件名为 test_assertions.py
def divide(a, b):
    assert b != 0, "divisor cannot be zero"
    return a / b
 
result = divide(10, 2)
print(f"Result: {result}")
 
result = divide(10, 0)  # 启用断言时会出现 AssertionError

正常运行:

bash
python test_assertions.py
# Output: Result: 5.0
# Then: AssertionError: divisor cannot be zero

使用优化运行:

bash
python -O test_assertions.py
# Output: Result: 5.0
# Then: ZeroDivisionError: division by zero

这就是为什么断言绝不能用于外部数据的验证——如果有人用 -O 运行你的程序,所有断言都会被跳过。断言只应用于在开发和测试期间捕获编程 bug。

条件为 True

条件为 False

代码执行

断言检查

继续执行

抛出 AssertionError
并附带消息

程序停止
显示回溯信息

开发者修复 bug

© 2025. Primesoft Co., Ltd.
support@primesoft.ai