跳转至

437_防御性编程技巧


一句话说明

防御性编程是"假设外部输入和调用都可能出错",提前做好断言、校验、异常处理,让代码健壮到能扛住各种意外攻击。


核心知识点

白话理解

就像过马路前先左右看——不是觉得一定有车,而是养成习惯。防御性编程也是这种思维:函数入口校验参数,调用外部服务加超时,操作文件加 try-except,让代码在任何输入下都不会"爆炸"。

核心技巧

  1. 断言(Assert):开发阶段快速暴露逻辑错误
  2. 输入校验:函数入口检查类型、范围、非空
  3. 异常处理:捕获具体异常,不用裸 except: 吞掉所有错
  4. 防御性拷贝:函数接收可变对象时先拷贝,防止外部修改影响内部状态
  5. 类型注解 + 运行时校验:Python 用 typing + pydantic 双保险

经典题目与解法

题目1:写一个健壮的除法函数,考虑所有边界

题意:实现 safe_divide(a, b),需要处理:非数字输入、除以零、溢出等情况。

from typing import Union

def safe_divide(a: Union[int, float], b: Union[int, float]) -> float:
    """
    防御性除法:处理所有异常情况
    """
    # 1. 类型校验:确保是数字类型
    if not isinstance(a, (int, float)):
        raise TypeError(f"参数a必须是数字,得到 {type(a).__name__}")
    if not isinstance(b, (int, float)):
        raise TypeError(f"参数b必须是数字,得到 {type(b).__name__}")

    # 2. 非法值校验:NaN 和 Infinity 不参与计算
    import math
    if math.isnan(a) or math.isnan(b):
        raise ValueError("参数不能是 NaN")
    if math.isinf(a) or math.isinf(b):
        raise ValueError("参数不能是无穷大")

    # 3. 除零校验
    if b == 0:
        raise ZeroDivisionError("除数不能为零")

    result = a / b

    # 4. 结果合法性校验
    if math.isinf(result):
        raise OverflowError("计算结果溢出")

    return result

# 测试各种边界
test_cases = [
    (10, 2),          # 正常
    (10, 0),          # 除零
    ("a", 2),         # 类型错误
    (float('nan'), 1),# NaN
    (float('inf'), 2),# 无穷
]

for a, b in test_cases:
    try:
        print(f"{a} / {b} = {safe_divide(a, b)}")
    except Exception as e:
        print(f"{a} / {b} 出错: {type(e).__name__}: {e}")

时间复杂度:O(1)
空间复杂度:O(1)


题目2:实现防御性字典访问工具,避免 KeyError 和类型错误

题意:从嵌套字典中安全取值,支持默认值,路径不存在时不崩溃。

from typing import Any, List

def safe_get(data: dict, keys: List[str], default=None) -> Any:
    """
    安全获取嵌套字典值
    例:safe_get(d, ['user', 'name', 'first'], '匿名') 
    相当于 d['user']['name']['first'],任何层级不存在返回默认值
    """
    # 断言输入类型
    assert isinstance(keys, list) and len(keys) > 0, "keys必须是非空列表"

    current = data                        # 从根节点开始
    for key in keys:
        if not isinstance(current, dict): # 当前节点不是字典,无法继续
            return default
        current = current.get(key)        # 安全取值,不存在返回None
        if current is None:               # 取到None,后续无法继续
            return default

    return current                        # 返回找到的值

# 防御性更新:修改前深拷贝,防止污染原始数据
import copy

def safe_update(data: dict, key: str, value: Any) -> dict:
    """
    返回更新后的新字典,不修改原始数据(防御性拷贝)
    """
    new_data = copy.deepcopy(data)        # 深拷贝,与原始数据完全隔离
    new_data[key] = value
    return new_data

# 测试
user = {
    'name': {'first': '张', 'last': '三'},
    'age': 25
}

print(safe_get(user, ['name', 'first']))         # 张
print(safe_get(user, ['address', 'city'], '未知')) # 未知(路径不存在)
print(safe_get(user, ['age', 'invalid'], 0))      # 0(age不是字典)

# 防御性更新
updated = safe_update(user, 'age', 26)
print(user['age'])     # 25,原始数据未被修改
print(updated['age'])  # 26

时间复杂度:O(k) k是键的层数
空间复杂度:O(n) n是数据大小(深拷贝)


面试技巧

  1. 区分断言和异常assert 用于开发时检查不变量(生产环境可禁用),raise 用于运行时错误处理
  2. 异常要具体:捕获 except ValueError 而不是裸 except:,否则连 KeyboardInterrupt 都被吞掉
  3. fail fast 原则:越早发现错误越好,函数入口校验优于深层崩溃
  4. 日志要充分:捕获异常后记录上下文信息,生产问题才能快速定位
  5. 不要静默失败:发现错误要么抛出要么记录,不能"假装没事"

速查表

技巧Python 写法说明
类型校验isinstance(x, int)检查类型
断言assert cond, "msg"开发期快速检查
安全取值d.get(key, default)避免 KeyError
异常捕获except (ValueError, TypeError) as e:具体异常
深拷贝copy.deepcopy(obj)防御性拷贝
类型注解def f(x: int) -> str:静态分析辅助
# 防御性编程模板
def robust_function(param):
    # 1. 类型检查
    if not isinstance(param, expected_type):
        raise TypeError(f"期望 {expected_type},得到 {type(param)}")
    # 2. 范围检查
    if param < 0 or param > MAX_VALUE:
        raise ValueError(f"param 超出范围 [0, {MAX_VALUE}]")
    # 3. 业务逻辑
    try:
        result = do_something(param)
    except SpecificException as e:
        logger.error(f"处理失败: {e}, param={param}")
        raise
    return result