Skip to content

01-错误与异常基础

Python 版本要求: 本教程基于 Python 3.11+ 编写,部分特性需要 3.11 或更高版本。

导航: 错误与异常基础异常处理抛出异常上下文管理器


概念铺垫

第一步:实际场景引入

为什么需要了解错误与异常?

假设你正在编写一个计算器程序:

python
def calculate(expression: str) -> float:
    """计算数学表达式"""
    return eval(expression)

# 用户输入
result = calculate(input("请输入表达式:"))

问题来了:

用户输入: 10 / 0
程序报错: ZeroDivisionError: division by zero
程序崩溃!💥

用户输入: abc
程序报错: SyntaxError
程序崩溃!💥

用户输入: (缺少括号
程序报错: SyntaxError: unexpected EOF
程序崩溃!💥

思考: 程序总是意外崩溃,用户体验极差。如何让程序优雅地处理这些错误?


第二步:概念动机

错误与异常的关系

┌─────────────────────────────────────────┐
│         错误与异常的关系                │
├─────────────────────────────────────────┤
│                                         │
│  错误(Error)                          │
│  ─────────────────                      │
│  程序执行过程中出现的问题               │
│                                         │
│  异常(Exception)                      │
│  ─────────────────                      │
│  错误的一种,程序可以捕获和处理         │
│                                         │
│  ─────────────────────────────────      │
│                                         │
│  错误的分类:                           │
│                                         │
│  错误                                   │
│  ├── 语法错误(SyntaxError)            │
│  │   代码写错了,无法运行               │
│  │                                      │
│  └── 运行时错误(Exception)            │
│      代码语法正确,但运行时出错         │
│      可以被捕获和处理                   │
│                                         │
└─────────────────────────────────────────┘

核心理解:

  • 语法错误:代码写错,解释器无法理解,必须修正才能运行
  • 异常:代码语法正确,但运行时遇到问题,可以被捕获处理

L1 理解层:会用

第三步:最简示例

语法错误 vs 运行时异常

python
# 语法错误:代码还没运行就报错
# if True   # ❌ SyntaxError: expected ':'

# 运行时异常:代码能运行,但执行时出错
result = 10 / 0  # ❌ ZeroDivisionError

区别:

类型发现时机能否捕获处理方式
语法错误运行前不能修正代码
异常运行时try-except

第四步:详细讲解

语法错误(SyntaxError)

语法错误在代码运行前就能发现,Python 解释器无法解析代码。

python
# ─────────────────────────────────────
# 示例 1:缺少冒号
# ─────────────────────────────────────
# if True
#     print("Hello")
# ❌ SyntaxError: invalid syntax

# 正确写法:
if True:
    print("Hello")

# ─────────────────────────────────────
# 示例 2:缺少括号
# ─────────────────────────────────────
# print("Hello"
# ❌ SyntaxError: unexpected EOF while parsing

# 正确写法:
print("Hello")

# ─────────────────────────────────────
# 示例 3:缩进错误
# ─────────────────────────────────────
# def greet():
# print("Hello")
# ❌ IndentationError: expected an indented block

# 正确写法:
def greet() -> None:
    print("Hello")

语法错误的特点:

  • 在代码运行前就能发现
  • Python 解释器无法解析代码
  • 无法通过 try-except 捕获
  • 必须修正代码才能运行

常见异常类型

异常类型速查

┌─────────────────────────────────────────┐
│          常见异常类型速查               │
├─────────────────────────────────────────┤
│                                         │
│  NameError                              │
│  使用了未定义的变量                     │
│  print(undefined_var)                   │
│                                         │
│  TypeError                              │
│  类型错误,操作应用于不适当的类型       │
│  "5" + 3  # 字符串不能和数字相加        │
│                                         │
│  ValueError                             │
│  值错误,类型正确但值不合适             │
│  int("abc")  # "abc"不能转为整数        │
│                                         │
│  IndexError                             │
│  索引越界                               │
│  [1, 2, 3][10]  # 索引超出范围          │
│                                         │
│  KeyError                               │
│  字典键不存在                           │
│  {"a": 1}["b"]  # 键 "b"不存在         │
│                                         │
│  AttributeError                         │
│  属性或方法不存在                       │
│  "str".not_exist()  # 方法不存在        │
│                                         │
│  FileNotFoundError                      │
│  文件不存在                             │
│  open("not_exist.txt")  # 文件不存在    │
│                                         │
│  ZeroDivisionError                      │
│  除以零                                 │
│  10 / 0  # 除零错误                     │
│                                         │
└─────────────────────────────────────────┘

异常示例详解

python
# ─────────────────────────────────────
# NameError: 变量未定义
# ─────────────────────────────────────
# print(name)  # name 未定义
# ❌ NameError: name 'name' is not defined

name: str = "Alice"
print(name)  # ✅ Alice

# ─────────────────────────────────────
# TypeError: 类型错误
# ─────────────────────────────────────
# result = "5" + 3
# ❌ TypeError: can only concatenate str (not "int") to str

# 正确写法:
result1: str = "5" + str(3)  # "53"
result2: int = int("5") + 3  # 8

# ─────────────────────────────────────
# ValueError: 值错误
# ─────────────────────────────────────
# age = int("abc")
# ❌ ValueError: invalid literal for int() with base 10: 'abc'

# 正确写法:
age: int = int("123")  # 123

# ─────────────────────────────────────
# IndexError: 索引越界
# ─────────────────────────────────────
numbers: list[int] = [1, 2, 3]
# value = numbers[10]
# ❌ IndexError: list index out of range

# 安全访问:
if len(numbers) > 10:
    value: int = numbers[10]

# ─────────────────────────────────────
# KeyError: 键不存在
# ─────────────────────────────────────
person: dict[str, int | str] = {"name": "Alice", "age": 25}
# city = person["city"]
# ❌ KeyError: 'city'

# 安全访问:
city: str = person.get("city", "未知")  # "未知"

# ─────────────────────────────────────
# ZeroDivisionError: 除零错误
# ─────────────────────────────────────
# result = 10 / 0
# ❌ ZeroDivisionError: division by zero

# 安全计算:
divisor: int = 0
if divisor != 0:
    result: float = 10 / divisor
else:
    result: float = float('inf')  # 无穷大

第五步:渐进复杂化

异常的层次结构

┌─────────────────────────────────────────┐
│          异常继承层次                   │
├─────────────────────────────────────────┤
│                                         │
│  BaseException                          │
│  └── Exception                          │
│      ├── ValueError                     │
│      ├── TypeError                      │
│      ├── IndexError                     │
│      ├── KeyError                       │
│      ├── FileNotFoundError              │
│      └── ...                            │
│                                         │
│  理解层次的好处:                       │
│  • 可以捕获一类异常                    │
│  • 可以精确捕获特定异常                │
│                                         │
└─────────────────────────────────────────┘

判断异常类型

python
def safe_divide(a: float, b: float) -> float | None:
    """安全除法,返回 None 表示错误"""
    try:
        return a / b
    except ZeroDivisionError:
        print("除数不能为零")
        return None
    except TypeError:
        print("参数类型错误")
        return None


# 使用
result1: float | None = safe_divide(10, 2)   # 5.0
result2: float | None = safe_divide(10, 0)   # None,打印错误
result3: float | None = safe_divide("10", 2)  # None,打印错误

第六步:实际应用

用户输入验证

python
def get_integer_input(prompt: str) -> int | None:
    """
    获取用户输入的整数。
    
    Args:
        prompt: 提示信息
        
    Returns:
        用户输入的整数,如果输入无效返回 None
    """
    try:
        user_input: str = input(prompt)
        return int(user_input)
    except ValueError:
        print("❌ 输入无效,请输入一个整数")
        return None


def main() -> None:
    """主程序"""
    print("=== 整数计算器 ===")
    
    while True:
        num1: int | None = get_integer_input("请输入第一个数(或 q 退出):")
        if num1 is None:
            continue
            
        num2: int | None = get_integer_input("请输入第二个数:")
        if num2 is None:
            continue
            
        try:
            result: int = num1 + num2
            print(f"结果:{num1} + {num2} = {result}")
        except Exception as e:
            print(f"计算错误:{e}")


if __name__ == "__main__":
    main()

文件读取安全处理

python
from pathlib import Path


def read_config(filepath: str) -> dict[str, str] | None:
    """
    安全读取配置文件。
    
    Args:
        filepath: 配置文件路径
        
    Returns:
        配置字典,如果文件不存在或格式错误返回 None
    """
    path: Path = Path(filepath)
    
    if not path.exists():
        print(f"❌ 配置文件不存在:{filepath}")
        return None
    
    try:
        content: str = path.read_text(encoding="utf-8")
        config: dict[str, str] = {}
        
        for line in content.strip().split("\n"):
            if "=" in line and not line.startswith("#"):
                key, value = line.split("=", 1)
                config[key.strip()] = value.strip()
                
        return config
        
    except Exception as e:
        print(f"❌ 读取配置文件失败:{e}")
        return None


# 使用
config: dict[str, str] | None = read_config("config.ini")
if config:
    print(f"配置加载成功:{config}")

关键代码说明:

代码含义为什么这样写
-> int | None返回值为整数或 None输入无效时返回 None 表示"无结果",比抛异常更适合交互式输入场景
except ValueError: return None捕获类型转换失败只捕获已知的具体异常,不用宽泛的 except Exception 掩盖真正的 bug
if not path.exists(): return None在 try 外提前检查文件存在性文件不存在是可预期情况,提前返回比捕获异常更清晰
line.split("=", 1)只切分一次配置值中可能含 =(如 URL),maxsplit=1 保证只拆成 key/value 两部分
except Exception as e: return None兜底捕获所有异常文件读取可能遇到编码错误、权限错误等,统一返回 None 而不让异常透传

L2 实践层:最佳实践

异常类型选择指南

如何选择合适的异常类型

┌─────────────────────────────────────────────────────────────────┐
│                    异常类型选择决策树                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  问题类型?                                                      │
│      │                                                          │
│      ├─→ 参数类型错误 ──→ TypeError                              │
│      │   例:函数期望 int,实际传入 str                          │
│      │                                                          │
│      ├─→ 参数值无效 ──→ ValueError                               │
│      │   例:int("abc")、负数年龄                                │
│      │                                                          │
│      ├─→ 索引/键不存在 ──→ IndexError / KeyError                 │
│      │   例:list[100]、dict["missing"]                          │
│      │                                                          │
│      ├─→ 资源不存在 ──→ FileNotFoundError / FileNotFoundError    │
│      │   例:文件、目录、网络资源                                │
│      │                                                          │
│      ├─→ 权限问题 ──→ PermissionError                            │
│      │   例:无写入权限、无执行权限                              │
│      │                                                          │
│      ├─→ 操作不支持 ──→ NotImplementedError                      │
│      │   例:子类未实现抽象方法                                  │
│      │                                                          │
│      └─→ 业务规则违反 ──→ 自定义异常                             │
│          例:余额不足、库存不够                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

推荐做法

场景推荐异常原因示例
类型不匹配TypeError明确指出类型问题"expected int, got str"
值超出范围ValueError类型正确但值不合法int("abc")、年龄为负
列表索引越界IndexError明确是索引问题list[100]
字典键缺失KeyError明确是键查找问题dict["missing"]
文件不存在FileNotFoundError明确是文件系统问题open("missing.txt")
功能未实现NotImplementedError明确是开发阶段问题抽象方法未实现
业务规则违反自定义异常业务语义清晰InsufficientBalanceError

异常类型的语义对比

┌─────────────────────────────────────────────────────────────────┐
│              TypeError vs ValueError vs 自定义异常              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  TypeError                                                      │
│  ─────────────────────────────────────────────────────────      │
│  含义:你给的类型和我期望的不一样                                │
│  场景:函数签名要求 int,你传了 str                              │
│  例:len(123)        # int 没有 __len__                         │
│      "5" + 3        # str 不能和 int 相加                       │
│                                                                 │
│  ValueError                                                     │
│  ─────────────────────────────────────────────────────────      │
│  含义:类型是对的,但值不合法                                    │
│  场景:int 类型正确,但 "abc"无法转换为整数                      │
│  例:int("abc")     # str 类型正确,值无法解析                   │
│      datetime(2024, 2, 30)  # 2月没有30号                       │
│                                                                 │
│  自定义异常                                                     │
│  ─────────────────────────────────────────────────────────      │
│  含义:业务层面的错误条件                                        │
│  场景:余额不足、库存不够、权限级别不够                          │
│  例:raise InsufficientBalanceError("余额不足")                  │
│      raise OutOfStockError("商品售罄")                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

常见异常陷阱

反模式:不要这样做

python
# ❌ 陷阱 1:捕获太宽泛的异常
try:
    result = some_operation()
except Exception:  # 捕获所有异常,包括系统异常
    print("出错了")

# 问题:
# 1. 捕获了不应该捕获的异常(如 KeyboardInterrupt)
# 2. 隐藏了真正的错误原因
# 3. 调试困难,无法定位问题
python
# ✅ 正确做法:捕获具体的异常
try:
    result = some_operation()
except ValueError as e:
    print(f"值错误:{e}")
except TypeError as e:
    print(f"类型错误:{e}")
python
# ❌ 陷阱 2:捕获后不做任何处理
try:
    data = json.loads(json_str)
except json.JSONDecodeError:
    pass  # 吞掉异常,继续执行

# 问题:
# 1. 错误被完全忽略
# 2. 后续代码可能使用无效数据
# 3. 问题会延迟暴露,更难排查
python
# ✅ 正确做法:至少记录日志
import logging

try:
    data = json.loads(json_str)
except json.JSONDecodeError as e:
    logging.error(f"JSON 解析失败:{e}")
    data = {}  # 使用默认值
python
# ❌ 陷阱 3:用异常控制正常流程
def find_item(items: list, target: str) -> int:
    try:
        return items.index(target)
    except ValueError:
        return -1

# 问题:
# 1. 异常比条件判断慢 10-100 倍
# 2. 用异常做"正常分支"违反语义
# 3. 异常本意是"异常情况",不是"控制流"
python
# ✅ 正确做法:先检查再操作
def find_item(items: list, target: str) -> int:
    if target in items:
        return items.index(target)
    return -1

# 或使用 dict.get() 模式
result = my_dict.get("key", default_value)  # 不抛异常
python
# ❌ 陷阱 4:在 finally 中返回值
def bad_function() -> int:
    try:
        return 1
    finally:
        return 2  # 会覆盖 try 的返回值!

# 问题:
# finally 总是执行,return 2 会覆盖 return 1
# 调用者永远得到 2,try 块的返回值被吞掉
python
# ✅ 正确做法:finally 只做清理
def good_function() -> int:
    resource = None
    try:
        resource = acquire_resource()
        return process(resource)
    finally:
        if resource:
            release_resource(resource)  # 只清理,不返回

常见陷阱汇总表

陷阱错误做法问题正确做法
捕获太宽泛except Exception:隐藏真正错误,吞掉系统异常捕获具体异常类型
空异常处理except: pass错误完全被忽略,延迟暴露至少记录日志或设置默认值
异常做控制流用异常做正常分支判断性能差,语义错误用条件判断或 .get()
finally 返回值finally: return x覆盖 try 的返回值finally 只做清理,不返回
捕获顺序错误Exception 后具体具体异常永远不执行从具体到宽泛的顺序
资源未释放异常发生但资源未关闭资源泄漏(文件、连接)with 语句或 finally

适用场景

异常处理的适用场景

场景是否推荐用异常原因替代方案
用户输入验证✅ 推荐输入错误是真正的异常情况-
文件/网络操作✅ 推荐外部资源不可控,异常是常态先检查 exists() 再操作
数据库操作✅ 推荐连接、事务失败需要处理-
API 调用✅ 推荐网络错误、认证失败需处理-
配置加载✅ 推荐配置缺失/错误影响启动提供默认配置
查找元素(不存在)❌ 不推荐不存在是常见的正常情况in 检查或 .get()
循环遍历❌ 不推荐遍历结束是正常行为for 循环自动处理
参数默认值❌ 不推荐默认值是设计的一部分直接设置默认值

查找操作的选择指南

python
# 场景:查找元素,可能不存在

# ❌ 用异常处理"不存在"(性能差)
try:
    value = my_dict["key"]
except KeyError:
    value = None

# ✅ 用 get() 方法(推荐)
value = my_dict.get("key")  # 不存在返回 None
value = my_dict.get("key", "default")  # 不存在返回默认值

# ✅ 用 in 检查(需要额外逻辑时)
if "key" in my_dict:
    value = my_dict["key"]
    process(value)
else:
    handle_missing()


L3 专家层:深入

Python 如何实现错误检测

Python 在运行时会经历四个阶段:源代码读取 → 解析(语法检查)→ 编译(字节码生成)→ 执行(PVM 运行)。语法错误在前两个阶段就被拦截,异常则发生在执行阶段。

Python 错误检测流程:
┌─────────────────────────────────────────────────────────────┐
│              Python 错误检测的时机                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  源码 .py                                                   │
│      │                                                      │
│      ▼                                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  解析器(Parser)                                     │   │
│  │  词法分析 → 语法分析 → AST 生成                       │   │
│  │                                                       │   │
│  │  此阶段捕获:                                         │   │
│  │  ・ SyntaxError(语法结构错误)                       │   │
│  │  ・ IndentationError(缩进错误)                       │   │
│  │                                                       │   │
│  │  无法通过 try-except 捕获!                            │   │
│  └─────────────────────────────────────────────────────┘   │
│      │ (解析通过)                                          │
│      ▼                                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  编译器(Compiler)                                   │   │
│  │  AST → 字节码对象 (__code__)                          │   │
│  └─────────────────────────────────────────────────────┘   │
│      │ (编译通过)                                          │
│      ▼                                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Python 虚拟机(PVM)                                 │   │
│  │  执行字节码                                          │   │
│  │                                                       │   │
│  │  此阶段发生:                                         │   │
│  │  ・ 所有 Exception 子类(ZeroDivisionError 等)      │   │
│  │  ・ 可以通过 try-except 捕获                         │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

验证代码:

python
import ast
import sys

# 演示语法错误在编译前被捕获
try:
    compile("if True", "<test>", "exec")
except SyntaxError as e:
    print(f"语法错误: {e}")
    print(f"发生于: 文件={e.filename}, 行={e.lineno}, 列={e.offset}")

# 演示运行时异常触发的位置
def trace_exception():
    try:
        10 / 0
    except ZeroDivisionError as e:
        print(f"异常类型: {type(e).__name__}")
        print(f"异常所在帧: {sys._getframe()}")

异常层次结构的实现

Python 使用 C 扩展类型系统实现异常类的继承关系:

异常对象创建流程:
┌─────────────────────────────────────────────────────────────┐
│              BaseException 的底层实现                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  BaseException.__new__()                                     │
│      │                                                      │
│      ├─ 设置 __traceback__ 属性                             │
│      ├─ 设置 __cause__ (异常链中的原因)                      │
│      ├─ 设置 __context__ (异常处理时的新异常)                │
│      ├─ 设置 args 元组                                      │
│      │                                                      │
│      └─ 被捕获时:                                          │
│         1. 解释器暂停当前帧                                 │
│         2. 沿调用栈向上查找匹配的 except 块                  │
│         3. 找到:将异常对象绑定到 as 变量                    │
│         4. 未找到:打印 traceback 并终止程序                │
│                                                             │
│  Exception vs BaseException:                                │
│  ───────────────────────────                                │
│  BaseException 子类:                                       │
│    KeyboardInterrupt, SystemExit, GeneratorExit             │
│    这些通常不应被捕获(裸 except: 除外,但不建议)          │
│                                                             │
│  Exception 子类:                                           │
│    所有"常规"异常(ValueError, TypeError...)               │
│    应该只捕获 Exception 及其子类                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

验证代码:

python
# 查看异常继承树
def show_hierarchy(exc_class: type, indent: int = 0) -> None:
    print("  " * indent + exc_class.__name__)
    for sub in exc_class.__subclasses__():
        show_hierarchy(sub, indent + 1)

print("BaseException 继承树(前两层):")
# show_hierarchy(BaseException)  # 取消注释查看完整树
print(f"  BaseException")
for sub in BaseException.__subclasses__():
    print(f"    {sub.__name__} ({len(sub.__subclasses__())} 个子类)")

性能考量

操作时间复杂度说明
SyntaxError 检测O(n)解析源代码,n 为代码行数
异常抛出(未发生)O(1)try 块设置几乎零开销(Python 3.11+ 零成本异常)
异常抛出(发生)O(d)d 为调用栈深度,需构建 traceback
isinstance 异常匹配O(m)m 为 except 子句数量,线性检查
异常链回溯O(c)c 为链中异常数量

Python 3.11+ 的零成本异常处理(Zero-Cost Exception Handling):

python
import dis

def example():
    try:
        x = 1
    except ValueError:
        pass

dis.dis(example)
# Python 3.11+ 输出:try 块内不再是 SETUP_FINALLY,而是用异常表
# 这意味着不抛出异常时 try 块完全零开销

异常查找链路

异常传播性能路径:
┌─────────────────────────────────────────────────────────────┐
│              异常时 vs 正常时的性能差异                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  正常路径(无异常):                                        │
│  PUSH_EXC_INFO(设置异常处理器)→ 执行代码               │
│  → POP_EXC_INFO → 正常结束                                  │
│  开销:~10ns(几乎可忽略)                                   │
│                                                             │
│  异常路径(有异常):                                        │
│  PUSH_EXC_INFO → 执行代码 → 抛出异常                     │
│  → 遍历调用栈(O(d)) → 构建 traceback                   │
│  → 匹配 except 子句(O(m)) → 设置 __traceback__         │
│  → 执行 except 块 → 清理异常状态                         │
│  开销:~10µs - 数ms(取决于栈深度)                         │
│                                                             │
│  差距:异常路径比正常路径慢 1000-100000 倍                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

设计动机

Python 为什么这样设计错误和异常体系?

设计选择原因替代方案对比
异常分为 BaseException 和 Exception系统级异常不应被业务代码捕获Java 的 Error vs Exception 类似
语法错误不可捕获代码无法运行,捕获无意义所有语言都如此
异常自动传播不会被忽略,不处理就终止Go 需要每条路径检查 err
异常是对象(携带丰富信息)方便调试和日志记录C 只用整数错误码
类型系统匹配异常isinstance 匹配支持继承C++ 和 Java 有类似机制

知识关联

错误与异常知识关联图:
                    ┌───────────────┐
                    │  解析器        │
                    │  语法检查      │
                    └───────────────┘


┌─────────────┐     ┌───────────────┐     ┌───────────────┐
│  编译阶段   │────→│  错误类型     │────→│  运行阶段    │
│  SyntaxError│     │  分类         │     │  Exception   │
└─────────────┘     └───────────────┘     └───────────────┘


                    ┌───────────────┐
                    │  BaseException│
                    │  继承体系     │
                    └───────────────┘


                    ┌───────────────┐
                    │  try-except   │
                    │  捕获机制     │
                    └───────────────┘


                    ┌───────────────┐
                    │  自定义异常   │
                    │  raise + from │
                    └───────────────┘

本章小结

┌─────────────────────────────────────────────────────────────┐
│                      错误与异常基础 知识要点                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   错误分类:                                                 │
│   ✓ 语法错误:代码写错,无法运行                            │
│   ✓ 运行时错误:运行时出现的异常                            │
│                                                             │
│   常见异常:                                                 │
│   ✓ NameError:变量未定义                                   │
│   ✓ TypeError:类型错误                                     │
│   ✓ ValueError:值错误                                      │
│   ✓ IndexError:索引越界                                    │
│   ✓ KeyError:键不存在                                      │
│   ✓ ZeroDivisionError:除零错误                             │
│                                                             │
│   处理方式:                                                 │
│   ✓ 语法错误:修正代码                                      │
│   ✓ 运行时异常:try-except 捕获                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘