【大模型2609·Python基础·10】健壮性与工程化:异常处理机制与模块包架构体系

【大模型2609·Python基础·10】健壮性与工程化:异常处理机制与模块包架构体系
kikock专栏导读:构建工业级高可用代码的必修课。前半部分系统剖析错误与异常分类、try-except-else-finally 完整防御结构、主动抛出与异常传播链、自定义业务异常;后半部分讲解规范的模块化工程组织(__name__、包规范目录结构、常用核心标准库及 setup.py 打包分发)。
第10章 错误和异常
10.1 错误和异常的介绍
Python是一门解释型语言,只有在程序运行后才会执行语法检查。所以,只有在运行或测试程序时,才会真正知道该程序能不能正常运行。
Python有两种错误很容易辨认:语法错误和异常。
10.1.1 语法错误
程序解析时遇到的语法错误。
1 | """ |
以上代码有语法错误:
1 | File "D:\shangguigu\PythonProject\chapter10\p01_syntax_error.py", line 4 |
10.1.2 异常
Python 程序的语法是正确的,在运行它的时候,也有可能发生错误。运行期检测到的错误被称为异常。
大多数的异常都不会被程序处理,都以错误信息的形式打印出来,错误信息的前面部分显示了异常发生的上下文,并以调用栈的形式显示具体信息。
1 | """ |
以上代码语法正确,但是运行报错:
1 | Traceback (most recent call last): |
10.2 异常处理
对异常进行处理并不是将错误规避了,而是当程序运行的时候,出现错误的时候提供解决方案,不终止程序,可以让程序继续执行。
10.2.1 try except
可以使用 try except 语句来捕获异常并处理。
语法格式:
1 | try: |
- 如果try没有发生异常,程序会忽略except中的代码,继续向下执行。
- 如果try发生了异常,会忽略try中剩余代码,根据异常类型匹配到相应的 except 并执行其中的代码。
- 如果try发生了异常,且异常类型无法和任何except匹配,也没有独立的
except:分支,异常将向外传递。 - 一个except可以同时处理多个异常,将这些异常放在一个元组中。
- 最后一个 except 可以忽略异常类型,它将被作为通配符使用。
演示案例
1 | """ |
10.2.2 获取异常描述信息
语法格式:
1 | try: |
- except的异常类型后面加as 变量名,当发生这个类型的异常时,异常对象就会被赋值给这个变量。单独的except后面不能加变量名。
- 打印traceback.print_exc()可以打印异常的详细信息
示例代码:
1 | """ |
10.2.3 else
可选地将else放在所有except之后。如果try中代码没有发生异常,将执行 else 中的代码。
语法格式:
1 | try: |
- **从执行效果上说,将代码放到else块和直接放到try块中是
一样的**。 - 将try正常执行完毕而没有引发任何异常后被执行的代码放到else中。提供了一种清晰的逻辑区分,将正常情况的代码与异常处理代码分开,使代码更易于理解和维护,有助于代码的可读性和可维护性。
示例代码:
1 | """ |
10.2.4 finally
可选地,放在最后。无论是否发生异常都会执行的代码,通常用于执行一些必须要进行的清理操作,例如关闭文件、释放资源(如网络连接、数据库连接、锁等),即使在执行 try 块中的代码时出现了异常,也能保证这些操作得以完成。
语法格式:
1 | try: |
- finally 语句块是 try-except 结构的一部分,它确保了无论 try 块中是否发生异常,也不管 except 块是否可以捕获异常,其中的代码都会被执行。
- 如果从执行效果上说,大部分场景,将代码放到finally语句和放到try-except块外效果是一样的。
- 但是直接放在try-except结构外面的代码只会在try-except结构正常执行完毕后才会执行,如果在try块中出现异常且没有被except块捕获,那么这部分代码将不会被执行。
示例代码:
1 | """ |
10.3 抛出异常
10.3.1 raise
当你想要在代码中明确表示发生了错误或异常情况时,可以使用 raise 来抛出异常。这可以帮助你在满足某些条件时停止程序的正常执行,并将控制权转移到异常处理部分。
语法格式:
1 | raise 异常类型("异常描述") |
案例演示:
1 | # 定义Account类 |
10.3.2 assert
assert用于判断一个表达式,在表达式条件为False的时候触发异常,常用于调试程序。
语法格式:
1 | assert 表达式 [,异常描述] |
等价于:
1 | if not 表达式: |
案例演示:
1 | """ |
10.4 异常的传递
当存在 try 嵌套或函数嵌套时,若内层出现了异常且在内层无法处理,会将异常一层一层向外传递,直到异常被处理或程序报错。
1 | # 定义计算三角形面积的函数 |
10.5 with
Python中的with语句用于异常处理,封装了try except finally编码范式,提供了一种简洁的方式来确保资源的正确获取和释放,同时处理可能发生的异常,提高了易用性。使代码更清晰、更具可读性,简化了文件流等公共资源的管理。
10.5.1 语法格式
语法格式:
1 | with expression as variable: |
- expression:通常是一个对象或函数调用,该对象需要是一个上下文管理器,即实现了
__enter__和__exit__方法。 - variable:是可选的,用于存储expression的
__enter__方法的返回值。
10.5.2 示例代码
1、未正确关闭资源
1 | # 定义函数:未在finally中关闭文件 |
2、在finally中正确关闭文件
1 | # 定义一个函数,处理文件 |
3、使用with关键字自动关闭文件
1 | # 定义一个函数,处理文件 |
10.5.3 工作原理
- 当执行with语句时,会调用expression对象的
__enter__方法。__enter__方法的返回值可以被存储在 variable 中(如果有),以供 with 代码块中使用。 - 然后执行 with 语句内部的代码块。
- 无论在代码块中是否发生异常,都会调用 expression 对象的
__exit__方法,以确保资源的释放或清理工作,这类似于 try-except-finally 中的 finally 子句。
close方法与__exit__方法的区别
close:是文件对象的普通方法,用于显式关闭文件,需要手动调用。如果在 close() 之前发生异常,文件可能不会被关闭。__exit__:是上下文管理器协议的特殊方法,在 with 语句结束时自动调用,由 Python 解释器自动调用。即使发生异常,也会确保执行清理操作。
10.6 异常类型
Python 所有异常类的顶层根类是 BaseException,所有异常都直接 / 间接继承它,核心分支分「系统级异常」和「业务级异常」,开发中常用的Exception是业务异常的父类。Python 异常继承的 3 个关键规则:
- 捕获父类 = 捕获所有子类:捕获某个异常类时,会自动捕获其所有直接 / 间接子类,比如捕获
LookupError,会同时捕获IndexError和KeyError: - 捕获顺序:先子类,后父类:Python 按
except的书写顺序匹配异常,若父类写在子类前面,子类异常会被父类 “拦截”,导致精准捕获失效: - 自定义异常必须继承 Exception(而非 BaseException)
常见的部分异常类型
| 异常 | 子类异常 | 子类异常 | 说明 | |
|---|---|---|---|---|
| BaseException(根) | SystemExit | sys.exit () 触发,程序正常退出,不建议捕获 | ||
| KeyboardInterrupt | Ctrl+C 触发,用户主动中断,不建议捕获 | |||
| Exception | 所有业务级异常的核心父类,开发中 99% 的场景捕获此类及其子类 | |||
| SyntaxError | 编译时语法错误(非运行时异常) | |||
| IndentationError | 缩进错误 | |||
| ArithmeticError | 算术运算通用异常,封装各类数值计算错误 | |||
| ZeroDivisionError | 除法 / 取模运算除数为 0,最常用的算术异常 | |||
| OverflowError | 数值运算结果超出可表示范围(主要浮点数) | |||
| LookupError | 「查找失败」通用父类,统一序列 / 字典的查找异常,便于批量捕获 | |||
| IndexError | 列表 / 元组 / 字符串等序列索引越界 | |||
| KeyError | 字典访问不存在的键 | |||
| OSError | Python3.3 + 整合 IOError,成为系统 / IO 通用异常,覆盖文件 / 系统相关错误 | |||
| FileNotFoundError | 只读模式打开不存在的文件 / 路径 | |||
| PermissionError | 对文件执行无权限操作(读 / 写 / 执行) | |||
| IsADirectoryError | 对目录执行文件操作(如 open 一个文件夹) | |||
| ImportError | 导入相关错误,Python3.6 + 新增子类 ModuleNotFoundError | |||
| NameError | 使用未定义的变量 / 函数 / 类,子类 UnboundLocalError(局部变量未绑定) | |||
| AttributeError | 访问对象不存在的属性 / 方法,或修改不可变对象属性 | |||
| TypeError | 类型不匹配(如数字 + 字符串相加)、函数传参类型错误 | |||
| ValueError | 类型正确但值非法(如 int (“abc”)),子类 UnicodeError(编码 / 解码错误) | |||
| StopIteration | 迭代器遍历完所有元素后,继续调用__next__() | |||
| AssertionError | assert 语句条件为 False,断言验证失败(调试用) |
10.7 自定义异常
通过直接或者间接继承Exception类来创建自己的异常。例如:
1 | """ |
第11章 模块和包
11.1 模块
11.1.1 模块概述
Python中一个以.py结尾的源文件即为一个模块(Module)。其中可以包含变量、函数和类等。通常情况下,我们把能够实现某一特定功能的代码放置在一个文件中作为一个模块。
使用模块提高了代码的可维护性,也提高了代码的复用性。即编写好一个模块后,只要是实现该功能的程序,都可以导入这个模块实现。另外,使用模块也可以避免名称冲突,相同名字的函数或变量可以分别存在与不同的模块中。
11.1.2 创建模块
模块名区分大小写,且不能与Python自带的标准模块重名。
自定义模块:p02_my_math_module.py
1 | """ |
自定义模块:p02_my_shape_module.py
1 | """ |
11.1.3 导入模块
1、模块搜索顺序
当导入一个模块时,会按照以下顺序进行查找:
(1)当前目录。
(2)PYTHONPATH环境变量中的目录。
(3)包含标准 Python 模块以及这些模块所依赖的任何 extension module 的目录。
(4)site-packages - 查找第三方包安装路径
可以使用以下方式查看模块搜索顺序:
1 | import sys |
路径顺序说明:
- sys.path 列表中的路径按照优先级顺序排列
- Python会从列表的第一个元素开始查找,直到找到所需的模块
- 如果在某个路径中找到了模块,就会停止搜索并加载该模块
- 可以通过 sys.path.append() 或 sys.path.insert() 动态修改搜索路径
这个查找机制确保了模块能够被正确加载,同时允许开发者自定义模块搜索路径
1 | import sys |
2、import 模块:导入模块所有成员
语法格式:
1 | import 模块名 【as 模块别名】 |
- 导入模块的所有成员,通过模块名.成员名的方式访问。即使多次使用 import导入同一模块,模块也只会被导入一次。
示例代码:
1 | """ |
3、from模块import成员:指定导入模块具体成员
语法格式:
1 | from 模块名 import 成员名1[as 别名], 成员名2[as 别名],… |
- 只能使用其导入的成员,未导入的成员不能使用。
- 指定导入模块的部分成员,直接通过成员名的方式访问,无需加
模块名.。 - 如果多个模块中存在重名成员,后一次导入会覆盖前一次导入。此时可以通过给成员取不同的别名来区分。
示例代码:
1 |
|
4、 from模块import *:导入模块所有非私有成员
语法格式:
1 | from 模块名 import * |
- 导入模块中所有不以单下划线开头的成员,直接通过成员名的方式访问。
示例代码:
1 |
|
5、__all__指定import *可导入成员
使用from import *导入模块时,可以在被导入的模块中使用 __all__设置哪些内容可以被导入。__all__ 的设置只针对使用 from import * 导入模块时有效。
创建模块:p06_my_math_module.py
1 | """ |
导入p06_my_math_module.py,测试
1 | """ |
6、使用__name__变量检查是否主模块
在 Python 中,__name__ 是一个特殊的内置变量
- 当一个Python文件被直接运行时,该文件的
__name__属性值为__main__。 - 当一个Python文件作为模块被导入时,
__name__属性会被设置为该模块的名称(即文件名,不包含 .py 后缀)。
有时我们会在模块中写一些测试代码,当模块被其他文件导入时这些测试代码会被执行。为了避免模块被导入时测试代码被执行,我们可以在被导入模块中添加对 __name__ 属性的检查:
1 | if __name__ == "__main__": |
创建模块:p07_my_math_module.py
1 | """ |
测试导入p07_my_math_module.py模块
1 | """ |
- 发现
p07_my_math_module模块中不在if __name__ == "__main__":中的测试代码被执行了
11.2 dir()函数
dir() 是一个内置函数,主要用于:
dir(对象):列出一个对象的名称列表(即属性和方法)dir(导入的模块名):列出一个模块中定义的名称列表(即函数、类、变量等)dir():列出当前作用域中定义的名称列表(即函数、类、变量等)
并以一个字符串列表的形式返回。
1 | """ |
11.3 包
11.3.1 包概述
包是一种管理 Python 模块命名空间的形式,包可以理解为是一个模块集。
通过使用包.模块名来构造Python模块命名空间的一种方式。例如,包名A.模块名B表示名为A的包中名为B的子模块。通常我们将多个有联系的模块放入一个包中。包与文件夹相似,不过该文件夹下必须有一个__init__.py文件。__init__.py 可以只是一个空文件,也可以执行包的初始化代码或设置 __all__ 变量。
假设要为统一处理声音文件与声音数据设计一个模块集(包)。
声音文件的格式很多(通常以扩展名来识别,例如:.wav,.aiff,.au),因此,为了不同文件格式之间的转换,需要创建和维护一个不断增长的模块集合。为了实现对声音数据的不同处理(例如:混声、添加回声、均衡器功能、创造人工立体声效果),还要编写无穷无尽的模块流。下面这个分级文件树展示了这个包的架构:
1 | sound/ 最高层级的包 |
11.3.2 通用的项目目录结构
1 | my_project/ # 项目根目录(工作目录) |
避坑关键:
1、关注Python 的搜索路径:解释器会按顺序在sys.path列表中查找包 / 模块,sys.path包含:当前工作目录、Python 安装目录的 site-packages(第三方包安装路径)、PYTHONPATH 环境变量路径等。可以通过print(sys.path)查看当前的搜索路径。
2、包的标识:必须有__init__.py文件(Python 3.3 + 可省略,但建议加,避免和普通文件夹混淆),该文件可以是空的,也可以写包的初始化代码(如批量导出模块、定义包的版本等)。
3、主程序的工作目录:运行 Python 代码时,工作目录是项目根是关键,否则会出现ModuleNotFoundError(模块找不到)。
11.3.3 准备工作:创建包
1、创建mygraphic包及其模块
(1)创建模块circle.py
1 | """circle.py""" |
(2)创建模块rectangle.py
1 | """rectangle.py""" |
2、创建mymath包及其模块
(1)basic_arithmetic.py
1 | def add(a,b): |
(2)advance_calculate.py
1 | def my_power(a,b): |
3、创建主运行文件
11.3.4 导入包的模块
1、import包
语法格式:
1 | import 包名 |
如果最后一项是包,那么必须在被导入包的__init__.py文件中,指定导入包中的哪些模块。这是python导包的一个优化机制,避免导入过多模块。并且在__init__.py中指定导入模块的时候,建议使用相对路径。例如:
1 | from . import basic_arithmetic # . 代表当前目录,即当前__init__.py文件的路径下找模块文件 |
如果不加from . ,它会把导入的模块basic_arithmetic作为顶级模块,直接从sys.path中是找basic_arithmetic模块,而不是找mymath包下的basic_arithmetic模块。
1 | import basic_arithmetic # 如果不加from . ,就相当于直接从sys.path中是找basic_arithmetic模块(会报错) |
主运行文件p01_package_test.py的代码
1 | """ |
2、import包.模块/from包import模块
语法格式:
1 | import 包名.模块名 [as 别名] |
主运行文件p02_package_test.py的代码
1 | # import mymath.advance_calculate as ma |
3、from包.模块import 成员
语法格式:
1 | from 包名.模块名 import 成员名 [as 别名] |
主运行文件p03_package_test.py的代码
1 | from mymath.advance_calculate import my_power as mpo,my_sqrt as msq |
4、__all__指定import*可导入模块
语法格式:
1 | from 包名.模块名 import * |
当我们使用 from import * 时,Python并不会查找并导入包的所有子模块,因为这将花费很长的时间,并且可能会产生我们不想要的副作用。
唯一的解决办法是提供包的显式索引。如果包的 __init__.py 中定义了 __all__,运行 from import * 时,它就是被导入的模块名列表。
mygraphic包
__init__.py中__all__定义如下:
1 | __all__ = ["circle"] |
主运行文件p04_package_test.py的代码
1 | from mygraphic import * |
11.4 标准库
标准库指的是在安装Python时就一同被安装的库。这些库经过精心挑选和开发,旨在为Python开发者提供通用且强大的工具集,涵盖各种不同的应用领域。更多标准库可参考https://docs.python.org/zh-cn/3/library/index.html。
11.4.1 常用标准库
| 名称 | 说明 |
|---|---|
| os | 多种操作系统接口。 |
| sys | 系统相关的形参和函数。 |
| time | 时间的访问和转换。 |
| datetime | 提供了用于操作日期和时间的类。 |
| math | 数学函数。 |
| random | 生成伪随机数。 |
| re | 正则表达式匹配操作。 |
| json | JSON 编码器和解码器。 |
| collections | 实现了一些专门化的容器,提供了对 Python 的通用内建容器 dict、list、set 和 tuple 的补充。 |
| functools | 高阶函数,以及可调用对象上的操作 |
| hashlib | 安全哈希与消息摘要。 |
| urllib | URL 处理模块。 |
| smtplib | SMTP 协议客户端,邮件处理。 |
| zlib | 与 gzip 兼容的压缩。 |
| gzip | 对 gzip 文件的支持。 |
| bz2 | 对 bzip2 压缩算法的支持。 |
| multiprocessing | 基于进程的并行。 |
| threading | 基于线程的并行。 |
| copy | 浅层及深层拷贝操作。 |
| socket | 低层级的网络接口。 |
| shutil | 提供了一系列对文件和文件集合的高阶操作,特别是提供了一些支持文件拷贝和删除的函数。 |
| glob | Unix 风格的路径名模式扩展。 |
14.4.2 日期时间库
示例
1 | from datetime import datetime |
常用格式指令:
| 格式指令 | 含义 | 示例 |
|---|---|---|
%Y |
4 位年份 | 2026 |
%y |
2 位年份(00-99) | 26(对应 2026) |
%m |
2 位月份(01-12) | 01(一月) |
%d |
2 位日期(01-31) | 24 |
%H |
24 小时制小时(00-23) | 10 |
%I |
12 小时制小时(01-12) | 10(上午) |
%M |
2 位分钟(00-59) | 30 |
%S |
2 位秒数(00-59) | 00 |
%A |
星期全称 | Saturday |
%a |
星期简称 | Sat |
%b |
月份简称 | Jan |
%B |
月份全称 | January |
%p |
上午 / 下午(AM/PM) | AM/PM(配合 % I 使用) |
11.5 引入第三方库
当需要使用Python中没有内置的库时,可以通过以下方式安装第三方库
11.5.1 pip命令方式
pip是Python包管理工具,该工具提供了对 Python 包的查找、下载、安装、卸载的功能。pip 默认的源是 Python Package Index(PyPI),其地址为 https://pypi.org/simple/,如果下载比较慢,还可以指定其它的源
- 阿里云:http://mirrors.aliyun.com/pypi/simple/
- 豆瓣:http://pypi.douban.com/simple/
- 清华大学:https://pypi.tuna.tsinghua.edu.cn/simple/
通过Windows命令行的方式安装,是将第三方包安装在本地python下,例如:D:\ProgramFiles\Python\Python312\Lib\site-packages
- 查看我们已经安装的软件包
1 | pip list |
- 安装软件包:具体包名就什么可以到PyPI上查找
1 | pip install 包名 # 安装指定包 |
- 卸载软件包
1 | pip uninstall 包名 |
- 临时使用其他源
1 | pip install -i http://mirrors.aliyun.com/pypi/simple/ 包名 |
- 永久修改源
1 | pip config set global.index-url http://mirrors.aliyun.com/pypi/simple/ |
11.5.2 PyCharm方式
通过Pycharm安装,我们选择的解释器类型每个项目独立的,所以是将第三方包安装在当前项目环境下,例如:D:\shangguigu\PythonProject.venv\Lib\site-packages
1)点击右下角的解释器设置
2)点击+
3)搜索包并安装
11.6 打包自己的库
11.6.1 先安装setuptools 库
如果不安装setuptools库,后续打包时可能会遇到报错 ModuleNotFoundError: No module named ‘distutils’,所以可以提前安装 setuptools 库。在命令提示符中执行如下命令:
1 | pip install setuptools |
11.6.2 编写setup.py文件
🔴在包外创建一个 setup.py 文件
1、打包多个完全独立的包
1 | from setuptools import setup |
2、总包下含多个子包
1 | from setuptools import setup, find_packages |
11.6.3 命令行方式打包
在 setup.py 同级目录下进行构建
1、build构建
1 | python setup.py build |
2、sdist构建为压缩包
1 | python setup.py sdist |
11.6.4 安装自己打包的库
1、命令行方式
命令行方式:以你自己打包的库的路径和压缩包名为准
1 | pip install graphic-1.0.tar.gz存储的路径名 |





























