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

专栏导读:构建工业级高可用代码的必修课。前半部分系统剖析错误与异常分类、try-except-else-finally 完整防御结构、主动抛出与异常传播链、自定义业务异常;后半部分讲解规范的模块化工程组织(__name__、包规范目录结构、常用核心标准库及 setup.py 打包分发)。


第10章 错误和异常

10.1 错误和异常的介绍

Python是一门解释型语言,只有在程序运行后才会执行语法检查。所以,只有在运行或测试程序时,才会真正知道该程序能不能正常运行。

Python有两种错误很容易辨认:语法错误和异常。

10.1.1 语法错误

程序解析时遇到的语法错误。

1
2
3
4
"""
演示语法错误
"""
print(

以上代码有语法错误:

1
2
3
4
  File "D:\shangguigu\PythonProject\chapter10\p01_syntax_error.py", line 4
print(
^
SyntaxError(语法错误): '(' was never closed

10.1.2 异常

Python 程序的语法是正确的,在运行它的时候,也有可能发生错误。运行期检测到的错误被称为异常。

大多数的异常都不会被程序处理,都以错误信息的形式打印出来,错误信息的前面部分显示了异常发生的上下文,并以调用栈的形式显示具体信息。

1
2
3
4
5
"""
演示异常
"""
result = 3 / 0
print(result)

以上代码语法正确,但是运行报错:

1
2
3
4
5
Traceback (most recent call last):
File "D:\shangguigu\PythonProject\chapter10\p02_exception_error.py", line 4, in <module>
result = 3 / 0
~~^~~
ZeroDivisionError: division by zero(除数为0的异常)

10.2 异常处理

对异常进行处理并不是将错误规避了,而是当程序运行的时候,出现错误的时候提供解决方案,不终止程序,可以让程序继续执行。

10.2.1 try except

可以使用 try except 语句来捕获异常并处理。

语法格式:

1
2
3
4
5
6
7
8
9
10
try:
可能发生异常的代码
except 异常类型1:
异常处理的代码
except 异常类型2:
异常处理的代码
except(异常类型3, 异常类型4, 异常类型5):
异常处理的代码
except:
异常处理的代码
  • 如果try没有发生异常,程序会忽略except中的代码,继续向下执行。
  • 如果try发生了异常,会忽略try中剩余代码,根据异常类型匹配到相应的 except 并执行其中的代码。
  • 如果try发生了异常,且异常类型无法和任何except匹配,也没有独立的except:分支,异常将向外传递。
  • 一个except可以同时处理多个异常,将这些异常放在一个元组中。
  • 最后一个 except 可以忽略异常类型,它将被作为通配符使用。

演示案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"""
异常处理
"""
while True:
try:
a = int(input("请输入被除数:"))
b = int(input("请输入除数:"))
print(f"{a} / {b} = {a/b}")
break
except (ValueError, TypeError):
print("必须输入整数")
except ZeroDivisionError:
print("除数不能为0")
except:
print("未知错误" )
print("End")

10.2.2 获取异常描述信息

语法格式:

1
2
3
4
5
6
7
8
9
10
try:
可能发生异常的代码
except 异常类型1 as 变量名1:
异常处理的代码
except 异常类型2 as 变量名2:
异常处理的代码
except(异常类型3, 异常类型4, 异常类型5) as 变量名3:
异常处理的代码
except:
异常处理的代码
  • except的异常类型后面加as 变量名,当发生这个类型的异常时,异常对象就会被赋值给这个变量。单独的except后面不能加变量名。
  • 打印traceback.print_exc()可以打印异常的详细信息

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"""
异常处理
"""
import traceback
while True:
try:
a = int(input("请输入第一个数字:"))
b = int(input("请输入第二个数字:"))
print(f"{a} / {b} = {a/b}")
break
except ValueError as e:
# traceback.print_exc() # 打印异常的详细信息
print("输入错误,请重新输入。异常信息是:",e)
except ZeroDivisionError as e:
print("除数不能为0。异常信息是:", e)
except:
print("未知错误")
print("End")

10.2.3 else

可选地将else放在所有except之后。如果try中代码没有发生异常,将执行 else 中的代码。

语法格式:

1
2
3
4
5
6
7
8
9
10
11
12
try:
可能发生异常的代码
except 异常类型1 【as 变量名1】: #【as 变量名1】表示可选的
异常处理的代码
except 异常类型2 【as 变量名2】:
异常处理的代码
except(异常类型3, 异常类型4, 异常类型5) 【as 变量名3】:
异常处理的代码
except:
异常处理的代码
else:
try中没有异常才执行的代码
  • **从执行效果上说,将代码放到else块和直接放到try块中是一样的**。
  • 将try正常执行完毕而没有引发任何异常后被执行的代码放到else中。提供了一种清晰的逻辑区分,将正常情况的代码与异常处理代码分开,使代码更易于理解和维护,有助于代码的可读性和可维护性。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"""
异常处理与else
"""
while True:
try:
a = int(input("请输入第一个数字:"))
b = int(input("请输入第二个数字:"))
result = a / b
except ValueError:
print("输入错误,请重新输入")
except ZeroDivisionError:
print("除数不能为0")
except:
print("未知错误" )
else:
print(f"{a} / {b} = {result}")
break

print("End")

10.2.4 finally

可选地,放在最后。无论是否发生异常都会执行的代码,通常用于执行一些必须要进行的清理操作,例如关闭文件、释放资源(如网络连接、数据库连接、锁等),即使在执行 try 块中的代码时出现了异常,也能保证这些操作得以完成。

语法格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try:
可能发生异常的代码
except 异常类型1 【as 变量名1】: #【as 变量名1】表示可选的
异常处理的代码
except 异常类型2 【as 变量名2】:
异常处理的代码
except(异常类型3, 异常类型4, 异常类型5) 【as 变量名3】:
异常处理的代码
except:
异常处理的代码
else:
try中没有异常才执行的代码
finally:
无论是否发生异常都会执行的代码
  • finally 语句块是 try-except 结构的一部分,它确保了无论 try 块中是否发生异常,也不管 except 块是否可以捕获异常,其中的代码都会被执行。
  • 如果从执行效果上说,大部分场景,将代码放到finally语句和放到try-except块外效果是一样的。
  • 但是直接放在try-except结构外面的代码只会在try-except结构正常执行完毕后才会执行,如果在try块中出现异常且没有被except块捕获,那么这部分代码将不会被执行。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"""
演示finally块的执行特点
"""

try:
a = int(input("请输入第一个数字:"))
b = int(input("请输入第二个数字:"))
result = a / b
except ValueError:
print("输入错误,请重新输入")
else:
print(f"{a} / {b} = {result}")
finally:
print("finally")

print("End")

10.3 抛出异常

10.3.1 raise

当你想要在代码中明确表示发生了错误或异常情况时,可以使用 raise 来抛出异常。这可以帮助你在满足某些条件时停止程序的正常执行,并将控制权转移到异常处理部分。

语法格式:

1
raise 异常类型("异常描述")

案例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 定义Account类
class Account:
def __init__(self, balance):
self.__balance = balance

def withdraw(self, amount):
if amount < 0:
raise ValueError("取款金额不能为负数")
if amount > self.__balance:
raise ValueError("余额不足")
self.__balance -= amount

def deposit(self, amount):
if amount < 0:
raise ValueError("存款金额不能为负数")
self.__balance += amount

@property
def balance(self):
return self.__balance

# 测试
account = Account(1000)
try:
account.withdraw(-500)
except ValueError as e:
print("取款失败,", e)

try:
account.deposit(-500)
except ValueError as e:
print("存款失败,", e)

try:
account.withdraw(1500)
except ValueError as e:
print("取款失败,", e)

try:
account.withdraw(500)
print("取款成功,余额为:", account.balance)
except ValueError as e:
print("取款失败,", e)

10.3.2 assert

assert用于判断一个表达式,在表达式条件为False的时候触发异常,常用于调试程序。

语法格式:

1
assert 表达式 [,异常描述]

等价于:

1
2
if not 表达式:
raise AssertionError([异常描述])

案例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"""
演示assert断言
"""
# 待测试函数
def add(a, b):
return a + b

def multiply(a, b):
return a * b

# 单元测试(轻量版)
def test_math_functions():
# 验证加法
assert add(2, 3) == 5, "加法函数错误:2+3应等于5"
assert add(-1, 1) == 0, "加法函数错误:-1+1应等于0"
# 验证乘法
assert multiply(2, 3) == 6, "乘法函数错误:2*3应等于6"
assert multiply(0, 5) == 0, "乘法函数错误:0*5应等于0"
print("所有测试用例通过!")

# 执行测试
test_math_functions() # 输出:所有测试用例通过!
# 若函数出错(比如multiply返回a+b),则触发断言:乘法函数错误:2*3应等于6

10.4 异常的传递

当存在 try 嵌套或函数嵌套时,若内层出现了异常且在内层无法处理,会将异常一层一层向外传递,直到异常被处理或程序报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 定义计算三角形面积的函数
def calculate_triangle_area(a, b, c):
"""计算三角形面积的函数"""
if a + b <= c or a + c <= b or b + c <= a or a <= 0 or b <= 0 or c <= 0:
raise ValueError("输入的数字无法构成三角形")

# 使用海伦公式计算面积
p = (a + b + c) / 2
area = (p * (p - a) * (p - b) * (p - c)) ** 0.5
return area

# 处理三角形的函数
def process_triangle():
"""处理三角形的函数"""
try:
a = int(input("请输入边长a的值:"))
b = int(input("请输入边长b的值:"))
c = int(input("请输入边长c的值:"))

# 调用计算面积函数
area = calculate_triangle_area(a, b, c)
print(f"三角形的面积为:{area}")

except ValueError:
print("输入错误,请重新输入")
raise # 重新抛出异常给上层


# 测试代码
while True:
try:
process_triangle()
break
except ValueError as e:
print(e)

10.5 with

Python中的with语句用于异常处理,封装了try except finally编码范式,提供了一种简洁的方式来确保资源的正确获取和释放,同时处理可能发生的异常,提高了易用性。使代码更清晰、更具可读性,简化了文件流等公共资源的管理。

10.5.1 语法格式

语法格式:

1
2
with expression as variable:
# 代码块
  • expression:通常是一个对象或函数调用,该对象需要是一个上下文管理器,即实现了 __enter__和__exit__方法。
  • variable:是可选的,用于存储expression的__enter__方法的返回值。

10.5.2 示例代码

1、未正确关闭资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定义函数:未在finally中关闭文件
def not_close_in_finally():
file = None
try:
# file = open("test.txt", "r")
file = open("test.txt", "w")
file.write('atguigu')
file.close()
print("文件写入成功")
except OSError as e:
print("文件写入失败:",e)
finally:
if file:
print("文件是否关闭:", file.closed) # 文件是否关闭: False
else:
print("文件未打开")

# 调用测试
not_close_in_finally()

2、在finally中正确关闭文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 定义一个函数,处理文件
def use_finally():
file = None
try:
# file = open("test.txt", "r")
file = open("test.txt", "w")
file.write('atguigu')
print("文件写入成功")
except OSError as e:
print("文件写入失败:",e)
finally:
if file:
file.close()
print("文件已关闭")
print("文件是否关闭:", file.closed) # 文件是否关闭: False
else:
print("文件未打开")

# 测试
use_finally()

3、使用with关键字自动关闭文件

1
2
3
4
5
6
7
8
9
10
11
12
# 定义一个函数,处理文件
def use_with():
try:
# with open("test.txt", "w") as file:
with open("test.txt", "r") as file:
file.write('atguigu')
print("文件写入成功")
finally:
print("文件是否关闭:", file.closed) # 文件是否关闭: False

# 测试
use_with()

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"""
自定义异常
"""
# 定义异常类
class NotATriangleError(Exception):
"""自定义异常类"""
def __init__(self, message):
self.message = message
def __str__(self):
return self.message

# 测试
while True:
try:
a = int(input("请输入第一个数字:"))
b = int(input("请输入第二个数字:"))
c = int(input("请输入第三个数字:"))
if a + b <= c or a + c <= b or b + c <= a or a<=0 or b<=0 or c<=0:
raise NotATriangleError("输入的数字无法构成三角形")
print("三角形的周长为:", a + b + c)
break
except ValueError as e:
print("输入错误,请重新输入整数值:",e)
except NotATriangleError as e:
print(e)

第11章 模块和包

11.1 模块

11.1.1 模块概述

Python中一个以.py结尾的源文件即为一个模块(Module)。其中可以包含变量、函数和类等。通常情况下,我们把能够实现某一特定功能的代码放置在一个文件中作为一个模块。

使用模块提高了代码的可维护性,也提高了代码的复用性。即编写好一个模块后,只要是实现该功能的程序,都可以导入这个模块实现。另外,使用模块也可以避免名称冲突,相同名字的函数或变量可以分别存在与不同的模块中。

11.1.2 创建模块

模块名区分大小写,且不能与Python自带的标准模块重名。

自定义模块:p02_my_math_module.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"""
自定义模块
"""
def add(a,b):
return a+b

def subtract(a,b):
return a-b

def multiply(a,b):
return a*b

def divide(a,b):
return a/b

def _remainder(a,b): # 为后面演示需要,故意以_开头
return a%b

自定义模块:p02_my_shape_module.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
"""
自定义模块
"""
def print_rectangle(width, height):
"""
打印矩形
:param width: 宽度
:param height: 高度
:return:
"""
for i in range(height):
for j in range(width):
print("*", end="")
print()

def print_diamond(lines):
"""
打印菱形
:param lines: 层数
:return:
"""
if not lines % 2 != 0:
raise ValueError("lines 必须是奇数")
center = int(lines // 2)
for i in range(lines):
for j in range(lines):
if abs(i-center) + abs(j-center) <= center:
print("*", end="")
else:
print(" ", end="")
print()

# 为了演示重名,故意与p01_my_math_module中的multiply()方法重名
def multiply(a, b):
"""
重复打印b次的a接收的数据
:param a: 要重复的数据
:param b: 重复b次
:return:
"""
print(f"{a}"* b)

11.1.3 导入模块

1、模块搜索顺序

当导入一个模块时,会按照以下顺序进行查找:

(1)当前目录。

(2)PYTHONPATH环境变量中的目录。

(3)包含标准 Python 模块以及这些模块所依赖的任何 extension module 的目录。

(4)site-packages - 查找第三方包安装路径

可以使用以下方式查看模块搜索顺序:

1
2
3
import sys

print(sys.path)

路径顺序说明:

  • sys.path 列表中的路径按照优先级顺序排列
  • Python会从列表的第一个元素开始查找,直到找到所需的模块
  • 如果在某个路径中找到了模块,就会停止搜索并加载该模块
  • 可以通过 sys.path.append() 或 sys.path.insert() 动态修改搜索路径

这个查找机制确保了模块能够被正确加载,同时允许开发者自定义模块搜索路径

1
2
3
4
5
6
7
8
9
import sys

print(sys.path)

sys.path.append("d:\\atguigu\\python")
sys.path.insert(0,"d:\\atguigu\\python")

print(sys.path)

2、import 模块:导入模块所有成员

语法格式:

1
import 模块名 【as 模块别名】
  • 导入模块的所有成员,通过模块名.成员名的方式访问。即使多次使用 import导入同一模块,模块也只会被导入一次。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"""
导入某个模块的全部成员
"""
import p02_my_math_module as my
import p02_my_shape_module

print(f"10+2={my.add(10,2)}")
print(f"10-2={my.subtract(10,2)}")
print(f"10*2={my.multiply(10,2)}")
print(f"10÷2={my.divide(10,2)}")
print(f"10%2={my._remainder(10,2)}")

print("打印9行菱形")
p02_my_shape_module.print_diamond(9)
print("打印9行5列矩形")
p02_my_shape_module.print_rectangle(9,5)
print("打印5个$")
p02_my_shape_module.multiply('$',5)

3、from模块import成员:指定导入模块具体成员

语法格式:

1
from 模块名 import 成员名1[as 别名], 成员名2[as 别名],…
  • 只能使用其导入的成员,未导入的成员不能使用。
  • 指定导入模块的部分成员,直接通过成员名的方式访问,无需加模块名.。
  • 如果多个模块中存在重名成员,后一次导入会覆盖前一次导入。此时可以通过给成员取不同的别名来区分。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

"""
指定导入模块的具体成员
"""
from p02_my_math_module import add,subtract as sub,multiply as mul,_remainder as mod
from p02_my_shape_module import multiply as mul_star

print(f"10+2={add(10,2)}")
print(f"10-2={sub(10,2)}")
print(f"10*2={mul(10,2)}")
# print(f"10÷2={div(10,2)}") # 报错, div() 未导入
print(f"10%2={mod(10,2)}")

print("打印5个$")
mul_star('$',5)

4、 from模块import *:导入模块所有非私有成员

语法格式:

1
from 模块名 import *
  • 导入模块中所有不以单下划线开头的成员,直接通过成员名的方式访问。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

"""
导入模块所有非私有成员
"""
from p02_my_math_module import *
from p02_my_shape_module import *

# 2个模块有重名成员,需要使用as 别名解决
from p02_my_math_module import multiply as mul_num
from p02_my_shape_module import multiply as mul_star

print(f"10+2={add(10,2)}")
print(f"10-2={subtract(10,2)}")
print(f"10*2={mul_num(10,2)}")
print(f"10÷2={divide(10,2)}")
# print(f"10%2={_remainder(10,2)}") # 报错,# import * 只能导入模块中所有不以单下划线开头的成员

print("打印9行菱形")
print_diamond(9)

print("打印5个$")
mul_star('$',5)

5、__all__指定import *可导入成员

使用from import *导入模块时,可以在被导入的模块中使用 __all__设置哪些内容可以被导入。__all__ 的设置只针对使用 from import * 导入模块时有效。

创建模块:p06_my_math_module.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"""
自定义模块
"""
__all__ = ["add","sub","mul"]

def add(a,b):
return a+b

def sub(a,b):
return a-b

def mul(a,b):
return a*b

def div(a,b):
return a/b

导入p06_my_math_module.py,测试

1
2
3
4
5
6
7
8
9
"""
演示__all__ + from import *
"""
from p06_my_math_module import *

print(f"10+2={add(10,2)}")
print(f"10-2={sub(10,2)}")
print(f"10*2={mul(10,2)}")
# print(f"10÷2={div(10,2)}") # 报错, div成员未在__all__中

6、使用__name__变量检查是否主模块

在 Python 中,__name__ 是一个特殊的内置变量

  • 当一个Python文件被直接运行时,该文件的__name__属性值为__main__。
  • 当一个Python文件作为模块被导入时,__name__属性会被设置为该模块的名称(即文件名,不包含 .py 后缀)。

有时我们会在模块中写一些测试代码,当模块被其他文件导入时这些测试代码会被执行。为了避免模块被导入时测试代码被执行,我们可以在被导入模块中添加对 __name__ 属性的检查:

1
2
if __name__ == "__main__":
测试代码

创建模块:p07_my_math_module.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""
自定义模块
"""
def add(a,b):
return a+b

def sub(a,b):
return a-b

def mul(a,b):
return a*b

def div(a,b):
return a/b

# 测试代码
print(f"10+2={add(10,2)}")
print(f"10-2={sub(10,2)}")
print(f"10*2={mul(10,2)}")
print(f"10÷2={div(10,2)}")

# 使用__name__变量检查是否主模块,如果是主模块,则执行测试代码
# if __name__ == "__main__":
# print(f"10+2={add(10, 2)}")
# print(f"10-2={sub(10,2)}")
# print(f"10*2={mul(10,2)}")
# print(f"10÷2={div(10,2)}")

测试导入p07_my_math_module.py模块

1
2
3
4
"""
演示导入其他模块,查看其他模块的测试代码是否被执行
"""
from p07_my_math_module import *
  • 发现p07_my_math_module模块中不在if __name__ == "__main__":中的测试代码被执行了

11.2 dir()函数

dir() 是一个内置函数,主要用于:

  • dir(对象):列出一个对象的名称列表(即属性和方法)
  • dir(导入的模块名):列出一个模块中定义的名称列表(即函数、类、变量等)
  • dir():列出当前作用域中定义的名称列表(即函数、类、变量等)

并以一个字符串列表的形式返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"""
演示dir()函数
"""
import math

# 查看math模块下的名称列表
print(f"math模块的名称列表:{dir(math)}")

# 自定义类
class Person:
home = "earth"

def __init__(self, name=None):
self.name = name

def eat(self):
print(f"{self.name} is eating")

# 创建对象
p = Person("张三")
print(f"p对象的名称列表:{dir(p)}")

# 定义函数
def test(info):
print(f"test函数作用域的名称列表:{dir()}") # dir()函数
# 调用函数
test("hello world")

print(f"当前模块作用域的名称列表:{dir()}")

11.3 包

11.3.1 包概述

包是一种管理 Python 模块命名空间的形式,包可以理解为是一个模块集。

通过使用包.模块名来构造Python模块命名空间的一种方式。例如,包名A.模块名B表示名为A的包中名为B的子模块。通常我们将多个有联系的模块放入一个包中。包与文件夹相似,不过该文件夹下必须有一个__init__.py文件。__init__.py 可以只是一个空文件,也可以执行包的初始化代码或设置 __all__ 变量。

假设要为统一处理声音文件与声音数据设计一个模块集(包)。

声音文件的格式很多(通常以扩展名来识别,例如:.wav,.aiff,.au),因此,为了不同文件格式之间的转换,需要创建和维护一个不断增长的模块集合。为了实现对声音数据的不同处理(例如:混声、添加回声、均衡器功能、创造人工立体声效果),还要编写无穷无尽的模块流。下面这个分级文件树展示了这个包的架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sound/                          最高层级的包
__init__.py 初始化 sound 包
formats/ 用于文件格式转换的子包
__init__.py
wavread.py
wavwrite.py
aiffread.py
aiffwrite.py
auread.py
auwrite.py
...
effects/ 用于音效的子包
__init__.py
echo.py
surround.py
reverse.py
...
filters/ 用于过滤器的子包
__init__.py
equalizer.py
vocoder.py
karaoke.py
...

11.3.2 通用的项目目录结构

1
2
3
4
5
6
7
8
9
10
11
my_project/  # 项目根目录(工作目录)
├── main.py # 自定义的主运行文件(入口)
├── pkg1/ # 自定义包1
│ ├── __init__.py
│ ├── mod1.py # 包1的模块1
│ └── sub_pkg/ # 包1的子包
│ ├── __init__.py
│ └── mod2.py # 子包的模块2
└── pkg2/ # 自定义包2
├── __init__.py
└── mod3.py # 包2的模块3

避坑关键:

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包及其模块

image-20260206114032886

image-20260206165905331

(1)创建模块circle.py

1
2
3
4
5
6
7
8
9
10
"""circle.py"""

PI = 3.1415926

def area(radius):
return PI * radius * radius

def perimeter(radius):
return 2 * PI * radius

(2)创建模块rectangle.py

1
2
3
4
5
6
7
8
"""rectangle.py"""

def area(width, height):
return width * height

def perimeter(width, height):
return 2 * (width + height)

2、创建mymath包及其模块

(1)basic_arithmetic.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def add(a,b):
return a+b

def subtract(a,b):
return a-b

def multiply(a,b):
return a*b

def divide(a,b):
return a/b

def remainder(a,b):
return a%b

(2)advance_calculate.py

1
2
3
4
5
def my_power(a,b):
return a ** b

def my_sqrt(a):
return a ** 0.5

3、创建主运行文件

image-20260206170457115

11.3.4 导入包的模块

1、import包

语法格式:

1
2
import 包名
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模块(会报错)

image-20260206171516140

主运行文件p01_package_test.py的代码

1
2
3
4
5
6
7
8
"""
# 演示导入包
"""
import mymath
print(mymath.basic_arithmetic.add(10,2)) # 正确,因为basic_arithmetic模块在mymath包的__init__.py文件中导入了
print(mymath.basic_arithmetic.subtract(10,2)) # 正确

# print(mymath.advance_calculate.my_power(2,3)) # 报错,因为advance_calculate模块未导入

2、import包.模块/from包import模块

语法格式:

1
2
3
4
5
import 包名.模块名 [as 别名]

# 或

from 包名 import 模块名 [as 别名]

主运行文件p02_package_test.py的代码

1
2
3
4
5
# import mymath.advance_calculate as ma
from mymath import advance_calculate as ma

print(ma.my_power(2,3))
print(ma.my_sqrt(9))

3、from包.模块import 成员

语法格式:

1
from 包名.模块名 import 成员名 [as 别名]

主运行文件p03_package_test.py的代码

1
2
3
4
from mymath.advance_calculate import my_power as mpo,my_sqrt as msq

print(mpo(2,3))
print(msq(9))

4、__all__指定import*可导入模块

语法格式:

1
from 包名.模块名 import *

当我们使用 from import * 时,Python并不会查找并导入包的所有子模块,因为这将花费很长的时间,并且可能会产生我们不想要的副作用。

唯一的解决办法是提供包的显式索引。如果包的 __init__.py 中定义了 __all__,运行 from import * 时,它就是被导入的模块名列表。

image-20260206174020575

mygraphic包__init__.py中__all__定义如下:

1
__all__ = ["circle"]

主运行文件p04_package_test.py的代码

1
2
3
4
5
6
7
from mygraphic import *

print("圆的周长:",circle.circumference(5))
print("圆的面积:",circle.area(5))

# print("矩形的周长:",rectangle.circumference(10,5)) # 报错
# 因为 mygraphic包的__init__.py文件中__all__没有指定rectangle模块

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from datetime import datetime

date1 = datetime(year=2026, month=1, day=1)
print("某一天", date1)

date2 = datetime.now()
print("今天:",date2)
print("获取年:", date2.year)
print("获取月份:",date2.month)
print("获取日期:",date2.day)
print("获取星期值:",date2.weekday())
print("获取星期单词",date2.strftime("%A"))
print("两个日期间隔:",date2 - date1)
str2 = date2.strftime("%Y年%m月%d日 %H:%M:%S")
print("日期转为字符串:", str2)

date3 = datetime.strptime("2026-05-01", "%Y-%m-%d")
print("字符串转为日期:" , date3)

time4 = datetime.timestamp(datetime.now())
print("日期转为时间戳:", time4)

常用格式指令:

格式指令 含义 示例
%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/,如果下载比较慢,还可以指定其它的源

通过Windows命令行的方式安装,是将第三方包安装在本地python下,例如:D:\ProgramFiles\Python\Python312\Lib\site-packages

  • 查看我们已经安装的软件包
1
pip list
  • 安装软件包:具体包名就什么可以到PyPI上查找
1
2
3
4
5
pip install 包名  # 安装指定包
pip install 包名==版本号 # 安装指定版本
pip install --upgrade 包名 # 升级包到最新版本
pip install -r requirements.txt # 批量安装requirements.txt文件中列出的包
pip install --user 包名 # 安装到用户目录
  • 卸载软件包
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/

image-20251227003129515

image-20251227003207380

image-20260226175934177

11.5.2 PyCharm方式

通过Pycharm安装,我们选择的解释器类型每个项目独立的,所以是将第三方包安装在当前项目环境下,例如:D:\shangguigu\PythonProject.venv\Lib\site-packages

1)点击右下角的解释器设置

image-20251227003343299

2)点击+

image-20251227003433917

3)搜索包并安装

image-20251227003730092

image-20251227004003669

image-20251227004027750

11.6 打包自己的库

11.6.1 先安装setuptools 库

如果不安装setuptools库,后续打包时可能会遇到报错 ModuleNotFoundError: No module named ‘distutils’,所以可以提前安装 setuptools 库。在命令提示符中执行如下命令:

1
pip install setuptools

image-20251227004808046

11.6.2 编写setup.py文件

🔴在包外创建一个 setup.py 文件

1、打包多个完全独立的包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from setuptools import setup

# 打包第一个独立包
def package1():
setup(
name="atguigu_graphic", # 打包后的包名称,必须唯一
version="0.1.0", # 版本说明
packages=["mygraphic"], # 手动指定第一个包的目录
python_requires=">=3.7", # python版本要求
)

# 打包第二个独立包
def package2():
setup(
name="atguigu_math",
version="0.1.0",
packages=["mymath"], # 手动指定第二个包的目录
python_requires=">=3.7",
)

if __name__ == "__main__":
# 依次执行两个包的打包
package1()
package2()

image-20260206182739974

2、总包下含多个子包

1
2
3
4
5
6
7
8
from setuptools import setup, find_packages

setup(
name="atguigu", # 打包后的包名称,必须唯一
version="0.1.0", # 版本号
packages=find_packages(), # 自动识别主包下的所有子包
python_requires=">=3.7", # 指定python版本
)

image-20260206183110644

11.6.3 命令行方式打包

在 setup.py 同级目录下进行构建

image-20260206183558457

1、build构建

1
python setup.py build

image-20260206183421804

2、sdist构建为压缩包

1
python setup.py sdist

image-20260206183532232

11.6.4 安装自己打包的库

1、命令行方式

命令行方式:以你自己打包的库的路径和压缩包名为准

1
pip install graphic-1.0.tar.gz存储的路径名

image-20260206184549248

2、PyCharm安装方式

image-20251227010549584

image-20251227010616451

image-20260206184816204

image-20260206184900031

image-20260206185242897