一、背景
上一篇整理了 Python 中的函数、参数、递归与变量作用域。
这一篇继续整理模块、包、文件操作和异常处理。对应的练习代码主要包括:
45模块简介.py
46模块的导入.py
47模块特性.py
48包管理.py
49文件对象.py
50文件打开方式.py
51异常和错误.py
52常见错误.py
53异常处理.py
54raise assert.py
55自定义异常.py
这部分内容开始从“写一个脚本”过渡到“组织一个项目”。
单个 .py 文件里写几十行代码还比较简单,一旦代码越来越多,就需要使用模块和包来拆分。程序运行过程中,也经常需要读写文件,并处理各种运行时异常。
二、模块是什么
从物理上看,一个 .py 文件就是一个模块。
从逻辑上看,模块是组织 Python 代码的一种方式。
例如有一个文件 utils.py:
def add(x, y):
return x + y
在另一个文件中可以导入它:
import utils
print(utils.add(1, 2))
模块的作用主要有三个:
- 复用代码
- 拆分代码结构
- 避免所有代码都堆在一个文件中
三、模块的命名空间
每个模块都有自己的命名空间。
例如:
import math
print(math.sqrt(9))
这里的 sqrt() 是 math 模块中的函数,所以通过 math.sqrt() 访问。
如果自己也写了一个模块,里面也有一个 sqrt() 函数,只要通过模块名区分,就不会直接冲突。
import math
import my_math
print(math.sqrt(9))
print(my_math.sqrt(9))
模块名就是一个很重要的命名空间边界。
四、模块搜索路径
导入模块时,Python 会按照一定的路径查找模块。
可以通过 sys.path 查看当前模块搜索路径:
import sys
for path in sys.path:
print(path)
如果模块找不到,通常会报:
ModuleNotFoundError: No module named 'xxx'
排查这类问题时,通常从下面几个方向看:
- 模块名是否写错
- 文件是否在当前项目路径中
- 是否少安装了第三方依赖
- 当前运行脚本的工作目录是否正确
sys.path中是否包含模块所在路径
运行过程中已经导入的模块,可以从 sys.modules 中看到。
import sys
print(sys.modules.keys())
不过实际开发中,不需要经常直接操作 sys.modules。
五、模块导入方式
常见导入方式有几种。
1. import 模块
import math
print(math.sqrt(16))
这种方式最清楚,推荐优先使用。
2. import 模块 as 别名
import math as m
print(m.sqrt(16))
别名一般用于模块名比较长,或者行业内有约定俗成的写法。
例如:
import pandas as pd
import numpy as np
3. from 模块 import 名称
from math import sqrt, fabs
print(sqrt(16))
print(fabs(-10))
这种写法可以少写模块名前缀,但是要注意名称冲突。
4. 不建议随意使用 import *
from math import *
这种写法会把模块中的很多名称直接导入当前命名空间,代码多了以后不容易判断一个函数来自哪里。
除非非常明确,否则不建议使用。
六、模块导入时会执行代码
模块第一次被导入时,模块中的顶层代码会被执行。
例如 demo.py:
print("demo module loaded")
def hello():
print("hello")
当其他文件执行:
import demo
会先打印:
demo module loaded
所以模块中一般不要把测试代码直接写在顶层,建议写到下面这个结构中:
def hello():
print("hello")
if __name__ == "__main__":
hello()
这样直接运行当前文件时会执行 hello(),被其他模块导入时不会执行测试代码。
七、包是什么
当模块很多时,可以使用包来组织。
从文件系统上看,包通常对应一个目录。
例如:
my_project/
main.py
utils/
__init__.py
file_utils.py
date_utils.py
导入方式:
from utils import file_utils
file_utils.read_file("demo.txt")
或者:
from utils.file_utils import read_file
read_file("demo.txt")
__init__.py 用来标识包,并且可以放一些包初始化逻辑。
在较新的 Python 版本中,没有 __init__.py 的目录也可能作为命名空间包使用,但是在学习和普通项目中,保留 __init__.py 更直观。
八、文件对象
文件对象可以理解为 Python 和外部文件之间的一层操作接口。
通过 open() 可以打开文件:
f = open("demo.txt", mode="r", encoding="utf-8")
content = f.read()
f.close()
不过这种写法有一个风险:如果读取过程中发生异常,close() 可能不会执行。
所以更推荐使用 with:
with open("demo.txt", mode="r", encoding="utf-8") as f:
content = f.read()
print(content)
with 语句结束后,会自动关闭文件。
九、读取文件
常见读取方式有三种。
1. read
一次性读取全部内容。
with open("demo.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
适合小文件。
2. readline
一次读取一行。
with open("demo.txt", "r", encoding="utf-8") as f:
line = f.readline()
print(line)
3. 直接遍历文件对象
实际处理日志或文本文件时,更常用这种写法:
with open("demo.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip())
这种方式不会一次性把整个文件加载到内存里。
十、写入文件
写文件可以使用 write()。
with open("demo.txt", "w", encoding="utf-8") as f:
f.write("hello python\n")
f.write("hello file\n")
需要注意:w 模式会清空原文件内容。
如果想追加内容,使用 a 模式。
with open("demo.txt", "a", encoding="utf-8") as f:
f.write("append line\n")
十一、文件指针 seek
文件对象内部有一个当前位置,可以使用 seek() 调整。
with open("demo.txt", "r", encoding="utf-8") as f:
f.seek(0)
content = f.read()
seek(0) 表示回到文件开头。
处理文本文件时,平时用得最多的是读、写、追加,seek() 使用相对少一些。处理二进制文件或固定格式文件时会更常见。
十二、文件打开模式
常见文件模式如下:
| 模式 | 含义 | 文件不存在时 |
|---|---|---|
r |
只读 | 报错 |
w |
写入,会清空原内容 | 创建 |
a |
追加写入 | 创建 |
r+ |
读写,不清空 | 报错 |
w+ |
读写,会清空原内容 | 创建 |
a+ |
追加读写 | 创建 |
rb |
二进制只读 | 报错 |
wb |
二进制写入 | 创建 |
几个容易混淆的点:
r只读,文件不存在会报错w会清空文件原内容a会追加到文件末尾b表示二进制模式,常用于图片、音频、压缩包等非文本文件- 文本文件建议明确指定
encoding="utf-8"
例如复制图片时要使用二进制模式:
with open("source.png", "rb") as src:
data = src.read()
with open("target.png", "wb") as dst:
dst.write(data)
十三、错误和异常
源码中把错误和异常做了区分。
可以简单理解为:
| 类型 | 说明 |
|---|---|
| 语法错误 | 代码不符合 Python 语法,程序无法正常解释 |
| 逻辑错误 | 程序能运行,但是结果不符合预期 |
| 异常 | 程序运行过程中出现的问题 |
例如语法错误:
# if True
# print("hello")
这种代码少了冒号,运行前就会被解释器检查出来。
例如运行时异常:
# print(10 / 0)
# ZeroDivisionError: division by zero
异常如果不处理,程序通常会终止。
十四、常见异常
常见异常可以先掌握下面这些。
| 异常 | 常见原因 |
|---|---|
SyntaxError |
语法错误 |
IndentationError |
缩进错误 |
NameError |
变量未定义 |
TypeError |
类型使用错误 |
ValueError |
值不合法 |
IndexError |
索引越界 |
KeyError |
字典 key 不存在 |
ZeroDivisionError |
除数为 0 |
FileNotFoundError |
文件不存在 |
ModuleNotFoundError |
模块不存在 |
示例:
nums = [1, 2, 3]
# print(nums[10])
# IndexError: list index out of range
字典 key 不存在:
user = {"name": "xiaoming"}
# print(user["age"])
# KeyError: 'age'
更稳妥的写法:
print(user.get("age", 0))
十五、try except
异常处理使用 try...except。
try:
result = 10 / 0
except ZeroDivisionError as e:
print("除数不能为 0:", e)
一般不建议直接写裸 except:
# 不推荐
try:
result = 10 / 0
except:
print("出错了")
裸 except 会捕获所有异常,不利于定位问题。
更推荐明确捕获具体异常。
try:
value = int("abc")
except ValueError as e:
print("转换失败:", e)
十六、try except else
else 会在没有异常时执行。
try:
value = int("123")
except ValueError as e:
print("转换失败:", e)
else:
print("转换成功:", value)
输出:
转换成功: 123
这种写法可以把正常逻辑和异常逻辑分开。
十七、try finally
finally 无论是否发生异常都会执行。
try:
f = open("demo.txt", "w", encoding="utf-8")
f.write("hello")
finally:
f.close()
不过文件操作中,更推荐使用 with,代码更简洁,也不容易忘记关闭文件。
with open("demo.txt", "w", encoding="utf-8") as f:
f.write("hello")
finally 更适合用在需要释放资源、关闭连接、清理临时状态的场景。
十八、raise 主动抛出异常
可以使用 raise 主动抛出异常。
nums = [1, 2, 3]
index = 5
if index >= len(nums):
raise IndexError("index 超出列表范围")
print(nums[index])
主动抛出异常适合在参数不合法、状态不符合要求时使用。
例如:
def divide(x, y):
if y == 0:
raise ValueError("y 不能为 0")
return x / y
十九、assert 断言
assert 用于断言某个条件必须成立。
age = 18
assert age > 0
如果条件为 False,会抛出 AssertionError。
# age = -1
# assert age > 0, "age 必须大于 0"
需要注意:assert 更适合用于调试和内部检查,不建议把它作为业务参数校验的唯一手段。
因为 Python 可以使用优化模式运行,优化模式下断言可能被跳过。
业务代码中更稳妥的写法是主动抛出明确异常:
def set_age(age):
if age <= 0:
raise ValueError("age 必须大于 0")
return age
二十、自定义异常
自定义异常通常继承 Exception。
class MyException(Exception):
def __init__(self, message):
super().__init__(message)
self.message = message
def __str__(self):
return f"自定义异常: {self.message}"
raise MyException("业务处理失败")
实际项目中,自定义异常常用于表达业务错误。
例如:
class ConfigError(Exception):
pass
def load_config(path):
if not path:
raise ConfigError("配置文件路径不能为空")
这样调用方就可以按业务异常进行捕获。
try:
load_config("")
except ConfigError as e:
print("配置错误:", e)
二十一、文件操作和异常处理结合
实际写文件读取逻辑时,文件操作经常和异常处理放在一起。
例如读取配置文件:
def read_config(path):
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
print("配置文件不存在:", path)
return ""
except UnicodeDecodeError:
print("配置文件编码不是 utf-8:", path)
return ""
content = read_config("app.conf")
这里分别处理了文件不存在和编码错误。
比起直接捕获所有异常,这种写法更容易排查问题。
二十二、总结
这一篇主要整理了模块、包、文件操作和异常处理。
需要重点记住:
- 一个
.py文件就是一个模块 - 包可以用目录组织多个模块
- 导入模块时,模块顶层代码会执行
- 测试代码建议放在
if __name__ == "__main__":中 - 文件操作优先使用
with open(...) as f r只读,w覆盖写,a追加写- 异常处理优先捕获具体异常,不建议裸
except raise可以主动抛出异常assert适合调试检查,不适合作为唯一业务校验- 自定义异常通常继承
Exception
这些内容是从脚本走向项目开发的基础,后面写面向对象、数据库操作、JSON 处理时还会继续用到。