Python 文件和异常完全指南
本文档面向零基础新手,目标是让你真正理解:
- 如何读取文件内容
- 如何写入和追加文件
with语句是什么、为什么要用- 什么是异常,程序报错时该怎么处理
try / except / else / finally各自的作用- 如何主动触发异常(raise)
- 如何创建自定义异常类
- 常见陷阱与注意事项
配有大量可运行示例。
第一部分:文件基础——打开和关闭
1.1 open() 函数
Python 用内置函数 open() 来打开文件:
文件对象 = open(文件路径, 模式, encoding=编码)
最关键的两个参数:
| 参数 | 说明 |
|---|---|
| 文件路径 | 要打开的文件路径(字符串) |
| 模式 | 打开方式(读/写/追加等,见下表) |
| encoding | 文本编码,中文文件一定要写 encoding="utf-8" |
常用模式:
| 模式 | 含义 | 文件不存在时 |
|---|---|---|
"r" |
只读(默认) | 报错 |
"w" |
只写,清空原有内容 | 自动创建 |
"a" |
追加(在末尾写) | 自动创建 |
"r+" |
读写 | 报错 |
"rb" |
二进制只读(图片等) | 报错 |
"wb" |
二进制只写 | 自动创建 |
1.2 不推荐的写法(忘记关闭文件)
# 这种写法有隐患:如果中途报错,文件可能不会被关闭
f = open("hello.txt", "r", encoding="utf-8")
content = f.read()
f.close() # 必须手动关闭,容易忘记或因报错跳过
1.3 推荐写法:with 语句(自动关闭)
# with 语句:代码块执行完毕后,无论是否报错,文件都会自动关闭
with open("hello.txt", "r", encoding="utf-8") as f:
content = f.read()
# 离开 with 块后,文件已自动关闭,不需要写 f.close()
请始终使用 with 语句打开文件! 这是 Python 的最佳实践。
第二部分:读取文件
2.1 准备示例文件
先手动创建一个文本文件 poem.txt,内容如下(保存为 UTF-8 编码):
静夜思
床前明月光,
疑是地上霜。
举头望明月,
低头思故乡。
2.2 read()——一次性读取全部内容
with open("poem.txt", "r", encoding="utf-8") as f:
content = f.read() # 返回整个文件内容,是一个字符串
print(content)
print(type(content)) # <class 'str'>
print(f"文件共 {len(content)} 个字符")
输出:
静夜思
床前明月光,
疑是地上霜。
举头望明月,
低头思故乡。
<class 'str'>
文件共 28 个字符
2.3 readlines()——读取所有行,返回列表
with open("poem.txt", "r", encoding="utf-8") as f:
lines = f.readlines() # 返回每一行组成的列表,每行末尾包含 n
print(lines)
# ['静夜思n', '床前明月光,n', '疑是地上霜。n', '举头望明月,n', '低头思故乡。']
# 去掉每行末尾的换行符
lines_clean = [line.rstrip() for line in lines]
print(lines_clean)
# ['静夜思', '床前明月光,', '疑是地上霜。', '举头望明月,', '低头思故乡。']
2.4 readline()——每次只读一行
with open("poem.txt", "r", encoding="utf-8") as f:
first_line = f.readline() # 读第一行
second_line = f.readline() # 读第二行
print(first_line.rstrip()) # 静夜思
print(second_line.rstrip()) # 床前明月光,
2.5 逐行迭代(推荐:内存占用最小)
with open("poem.txt", "r", encoding="utf-8") as f:
for line in f: # 文件对象本身是可迭代的,逐行读取
print(line.rstrip()) # rstrip() 去掉末尾换行符
输出:
静夜思
床前明月光,
疑是地上霜。
举头望明月,
低头思故乡。
什么时候用哪种读取方式?
| 方法 | 适合场景 |
|---|---|
read() |
文件较小,需要整体处理 |
readlines() |
文件较小,需要按行处理 |
for line in f |
文件可能很大,逐行处理,节省内存 |
readline() |
需要手动控制读几行 |
第三部分:写入文件
3.1 写入模式("w")——覆盖原内容
with open("output.txt", "w", encoding="utf-8") as f:
f.write("第一行内容n") # write() 不自动换行,需要手动加 n
f.write("第二行内容n")
f.write("第三行内容n")
# 验证:读回来看看
with open("output.txt", "r", encoding="utf-8") as f:
print(f.read())
输出:
第一行内容
第二行内容
第三行内容
注意: 用 "w" 模式打开已有文件时,原有内容会被清空!
3.2 writelines()——一次写入多行
lines = ["苹果n", "香蕉n", "草莓n", "葡萄n"]
with open("fruits.txt", "w", encoding="utf-8") as f:
f.writelines(lines) # 写入列表中的所有字符串,注意自己加 n
with open("fruits.txt", "r", encoding="utf-8") as f:
print(f.read())
3.3 追加模式("a")——在末尾添加内容
# 先创建文件并写入初始内容
with open("log.txt", "w", encoding="utf-8") as f:
f.write("日志开始n")
# 多次追加(不会清空原内容)
with open("log.txt", "a", encoding="utf-8") as f:
f.write("第一条记录n")
with open("log.txt", "a", encoding="utf-8") as f:
f.write("第二条记录n")
# 查看最终内容
with open("log.txt", "r", encoding="utf-8") as f:
print(f.read())
输出:
日志开始
第一条记录
第二条记录
3.4 写入 vs 追加的区别
"w"(写入):打开文件时清空所有内容,从头写
"a"(追加):打开文件时不清空,从末尾接着写
第四部分:文件路径
4.1 相对路径 vs 绝对路径
# 相对路径:相对于程序运行的当前目录
with open("data.txt", "r", encoding="utf-8") as f: # 当前目录下的 data.txt
pass
with open("data/scores.txt", "r", encoding="utf-8") as f: # 子目录
pass
# 绝对路径:从磁盘根目录开始的完整路径
with open("C:/Users/Administrator/Desktop/data.txt", "r", encoding="utf-8") as f:
pass
4.2 用 pathlib 处理路径(推荐)
from pathlib import Path
# 当前目录
base = Path(".")
# 用 / 拼接路径(跨平台,Windows/Mac/Linux 都能用)
file_path = base / "data" / "scores.txt"
# 检查文件是否存在再打开
if file_path.exists():
content = file_path.read_text(encoding="utf-8")
print(content)
else:
print(f"文件不存在:{file_path}")
4.3 Windows 路径的反斜杠问题
Windows 路径使用 ,但 在 Python 字符串里是转义字符(如 n 是换行),有两种解决方案:
# 方法一:用原始字符串 r"..."(推荐)
path = r"C:UsersAdministratorDesktopdata.txt"
# 方法二:用正斜杠 /(Python 在 Windows 也支持)
path = "C:/Users/Administrator/Desktop/data.txt"
# 方法三:用双反斜杠 \
path = "C:\Users\Administrator\Desktop\data.txt"
第五部分:什么是异常?
5.1 程序运行时的错误
当 Python 执行代码时遇到无法处理的情况,就会抛出(raise)一个异常,并停止执行:
# 除以零
print(10 / 0)
# ZeroDivisionError: division by zero
# 访问不存在的列表下标
lst = [1, 2, 3]
print(lst[10])
# IndexError: list index out of range
# 打开不存在的文件
open("不存在的文件.txt", "r")
# FileNotFoundError: [Errno 2] No such file or directory: '不存在的文件.txt'
# 类型错误
print("年龄:" + 18)
# TypeError: can only concatenate str (not "int") to str
这些都是异常(Exception),如果不处理,程序会崩溃退出。
5.2 异常的组成
一个异常有三个关键信息:
- 异常类型:如
FileNotFoundError、ValueError - 异常信息:具体描述错误原因
- Traceback(追踪信息):显示错误发生在哪一行
Traceback (most recent call last):
File "main.py", line 3, in <module>
print(lst[10])
IndexError: list index out of range
第六部分:try / except——捕获异常
6.1 基本语法
try:
# 可能发生异常的代码
...
except 异常类型:
# 发生异常时执行的代码
...
6.2 最简单的例子
# 不处理异常:程序崩溃
result = 10 / 0 # ZeroDivisionError,程序停止!
print("这行不会执行")
# 处理异常:程序继续
try:
result = 10 / 0
except ZeroDivisionError:
print("不能除以零!")
print("程序继续执行") # 这行会正常执行
输出:
不能除以零!
程序继续执行
6.3 捕获异常信息(as e)
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"发生了除零错误:{e}")
# 发生了除零错误:division by zero
as e 把异常对象赋值给变量 e,可以打印或记录详细信息。
6.4 处理文件读取异常
filename = "不存在的文件.txt"
try:
with open(filename, "r", encoding="utf-8") as f:
content = f.read()
print(content)
except FileNotFoundError:
print(f"文件 '{filename}' 不存在,请检查路径。")
6.5 捕获多种异常
方法一:多个 except 块(推荐,针对不同错误分别处理)
def divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
print("错误:除数不能为零!")
except TypeError:
print("错误:只能对数字进行除法!")
divide(10, 0) # 错误:除数不能为零!
divide(10, "abc") # 错误:只能对数字进行除法!
divide(10, 2) # 正常返回 5.0
方法二:一个 except 捕获多种(用元组)
try:
# 某些操作
pass
except (ZeroDivisionError, ValueError, TypeError) as e:
print(f"发生了数值或类型错误:{e}")
方法三:捕获所有异常(Exception)
try:
result = int("abc")
except Exception as e:
print(f"发生了某种错误:{type(e).__name__}: {e}")
# 发生了某种错误:ValueError: invalid literal for int() with base 10: 'abc'
注意: 不要滥用
except Exception,太宽泛会隐藏 bug。能明确异常类型就写明确的。
第七部分:try / except / else / finally
7.1 完整语法
try:
# 可能发生异常的代码
...
except 异常类型:
# 发生异常时执行
...
else:
# 没有发生任何异常时执行(try 块正常完成时)
...
finally:
# 无论是否发生异常,都会执行(通常做清理工作)
...
7.2 else 块的作用
else 只在 try 块没有发生任何异常时才执行:
def read_number_from_file(filename):
try:
with open(filename, "r", encoding="utf-8") as f:
number = int(f.read().strip())
except FileNotFoundError:
print(f"文件 '{filename}' 不存在")
except ValueError:
print("文件内容不是合法的整数")
else:
# 只有 try 里的代码全部成功,才会执行这里
print(f"成功读取到数字:{number}")
return number
# 测试
read_number_from_file("number.txt") # 如果文件不存在,打印提示
7.3 finally 块的作用
finally 无论是否发生异常都会执行,常用于释放资源、关闭连接等:
def process_file(filename):
print("开始处理文件...")
f = None
try:
f = open(filename, "r", encoding="utf-8")
content = f.read()
result = int(content.strip())
print(f"读取到数字:{result}")
except FileNotFoundError:
print(f"文件不存在:{filename}")
except ValueError:
print("文件内容不是数字")
finally:
# 无论成功还是失败,这里都会执行
if f:
f.close()
print("文件已关闭(finally 块执行)")
print("处理结束")
process_file("number.txt")
实际上,使用
with语句就不需要在finally里手动关闭文件了,with已经自动处理了。
7.4 四个关键字汇总
try:
print("1. try 里的代码运行")
# x = 1 / 0 # 取消注释来测试异常
except ZeroDivisionError:
print("2. 发生了 ZeroDivisionError,进入 except")
else:
print("3. 没有发生异常,进入 else")
finally:
print("4. 无论如何,finally 都执行")
print("5. 程序继续")
没有异常时的输出:
1. try 里的代码运行
3. 没有发生异常,进入 else
4. 无论如何,finally 都执行
5. 程序继续
有异常时的输出:
1. try 里的代码运行
2. 发生了 ZeroDivisionError,进入 except
4. 无论如何,finally 都执行
5. 程序继续
第八部分:常见内置异常类型
8.1 速查表
| 异常类型 | 触发场景 | 示例 |
|---|---|---|
FileNotFoundError |
打开不存在的文件 | open("no.txt") |
PermissionError |
没有权限读写文件 | 系统文件被只读 |
ValueError |
值的类型对,但内容不合适 | int("abc") |
TypeError |
类型不匹配 | "3" + 3 |
ZeroDivisionError |
除以零 | 1 / 0 |
IndexError |
列表下标越界 | [1,2][5] |
KeyError |
字典键不存在 | {"a":1}["b"] |
AttributeError |
对象没有该属性/方法 | None.split() |
NameError |
变量未定义 | print(x)(x未定义) |
ImportError |
导入模块失败 | import 不存在的模块 |
StopIteration |
迭代器已耗尽 | next(iter([])) |
RecursionError |
递归太深(超过限制) | 无终止条件的递归 |
MemoryError |
内存不足 | 创建超大数据结构 |
OSError |
操作系统相关错误的基类 | 磁盘满了、路径太长等 |
8.2 示例:常见异常
# ValueError
try:
age = int(input("请输入年龄:")) # 输入 "abc"
except ValueError:
print("输入的不是合法整数!")
# KeyError
student = {"name": "张三", "age": 18}
try:
grade = student["grade"]
except KeyError as e:
print(f"键 {e} 不存在!")
# IndexError
scores = [85, 90, 78]
try:
print(scores[10])
except IndexError:
print("下标越界!")
# AttributeError
try:
result = None.upper()
except AttributeError as e:
print(f"属性错误:{e}")
第九部分:raise——主动触发异常
9.1 为什么要主动触发异常?
有时候,你希望在代码发现不合理的情况时,主动报错来阻止程序继续执行错误的操作。
def set_age(age):
if age < 0 or age > 150:
raise ValueError(f"年龄 {age} 不合理,应在 0~150 之间")
print(f"年龄设置为:{age}")
set_age(25) # 年龄设置为:25
set_age(-5) # ValueError: 年龄 -5 不合理,应在 0~150 之间
set_age(200) # ValueError: 年龄 200 不合理,应在 0~150 之间
9.2 在函数中使用 raise
def divide(a, b):
if b == 0:
raise ZeroDivisionError("除数不能为零,请传入非零值")
return a / b
# 调用者捕获异常
try:
result = divide(10, 0)
except ZeroDivisionError as e:
print(f"捕获到错误:{e}")
9.3 重新抛出异常(raise 不带参数)
在 except 块里,单独写 raise 会把当前捕获的异常重新抛出:
def read_file(filename):
try:
with open(filename, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError as e:
print(f"[日志] 文件读取失败:{e}")
raise # 记录日志后,把异常继续往上抛
第十部分:自定义异常类
10.1 为什么要自定义异常?
内置异常(如 ValueError)是通用的,描述不够具体。自定义异常可以让错误信息更清晰:
# 不好:错误类型太通用
raise ValueError("余额不足")
# 好:一眼看出是银行业务的余额不足错误
raise InsufficientFundsError("余额不足,当前余额:100,需要:200")
10.2 创建自定义异常
自定义异常类必须继承自 Exception(或其子类):
# 最简单的自定义异常
class MyError(Exception):
pass
raise MyError("发生了自定义错误")
10.3 带属性的自定义异常
class InsufficientFundsError(Exception):
"""余额不足异常"""
def __init__(self, balance, amount):
self.balance = balance # 当前余额
self.amount = amount # 需要的金额
message = f"余额不足!当前余额:{balance} 元,需要:{amount} 元,缺少:{amount - balance} 元"
super().__init__(message) # 把消息传给父类
class AgeError(Exception):
"""年龄不合法异常"""
def __init__(self, age, min_age=0, max_age=150):
self.age = age
message = f"年龄 {age} 不合法,应在 {min_age}~{max_age} 之间"
super().__init__(message)
10.4 使用自定义异常
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
message = f"余额不足!当前余额:{balance} 元,需要:{amount} 元"
super().__init__(message)
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
print(f"取款 {amount} 元成功,剩余余额:{self.balance} 元")
# 使用
account = BankAccount("张三", 500)
try:
account.withdraw(200) # 成功
account.withdraw(400) # 余额不足,触发自定义异常
except InsufficientFundsError as e:
print(f"错误:{e}")
print(f"当前余额:{e.balance},尝试取出:{e.amount}")
输出:
取款 200 元成功,剩余余额:300 元
错误:余额不足!当前余额:300 元,需要:400 元
当前余额:300,尝试取出:400
第十一部分:文件与异常结合实战
11.1 安全读取文件
def safe_read_file(filename):
"""安全读取文件,失败时返回 None 并打印提示"""
try:
with open(filename, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
print(f"[错误] 文件不存在:{filename}")
return None
except PermissionError:
print(f"[错误] 没有权限读取:{filename}")
return None
except UnicodeDecodeError:
print(f"[错误] 文件编码不是 UTF-8:{filename}")
return None
content = safe_read_file("poem.txt")
if content:
print(content)
else:
print("读取失败,无内容可显示。")
11.2 安全写入文件
import os
def safe_write_file(filename, content, mode="w"):
"""安全写入文件,支持写入和追加"""
try:
# 如果是写入模式,且文件已存在,先确认
if mode == "w" and os.path.exists(filename):
print(f"警告:'{filename}' 已存在,将被覆盖!")
with open(filename, mode, encoding="utf-8") as f:
f.write(content)
print(f"[成功] 内容已写入 '{filename}'")
except PermissionError:
print(f"[错误] 没有权限写入:{filename}")
except OSError as e:
print(f"[错误] 写入失败:{e}")
safe_write_file("output.txt", "第一行内容n第二行内容n")
safe_write_file("output.txt", "追加的内容n", mode="a")
11.3 实战:用户注册信息持久化
import json
import os
USER_FILE = "users.json"
class UserExistsError(Exception):
"""用户名已存在异常"""
pass
class UserNotFoundError(Exception):
"""用户不存在异常"""
pass
def load_users():
"""从文件加载用户数据"""
if not os.path.exists(USER_FILE):
return {}
try:
with open(USER_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
print("[警告] 用户数据文件损坏,重置为空")
return {}
def save_users(users):
"""保存用户数据到文件"""
with open(USER_FILE, "w", encoding="utf-8") as f:
json.dump(users, f, ensure_ascii=False, indent=2)
def register(username, password, email):
"""注册新用户"""
users = load_users()
if username in users:
raise UserExistsError(f"用户名 '{username}' 已被注册")
users[username] = {
"password": password,
"email": email
}
save_users(users)
print(f"注册成功!欢迎,{username}!")
def login(username, password):
"""用户登录"""
users = load_users()
if username not in users:
raise UserNotFoundError(f"用户 '{username}' 不存在")
if users[username]["password"] != password:
raise ValueError("密码错误!")
print(f"登录成功!欢迎回来,{username}!")
# 测试
try:
register("张三", "password123", "zhangsan@example.com")
register("李四", "abc456", "lisi@example.com")
register("张三", "other_pass", "other@example.com") # 重复注册
except UserExistsError as e:
print(f"注册失败:{e}")
print()
try:
login("张三", "password123") # 成功
login("王五", "any_pass") # 用户不存在
except UserNotFoundError as e:
print(f"登录失败:{e}")
except ValueError as e:
print(f"登录失败:{e}")
输出:
注册成功!欢迎,张三!
注册成功!欢迎,李四!
注册失败:用户名 '张三' 已被注册
登录成功!欢迎回来,张三!
登录失败:用户 '王五' 不存在
第十二部分:常见陷阱与注意事项
12.1 陷阱一:用 "w" 模式打开会清空文件
# 危险!如果 important.txt 里有数据,以下代码会清空它!
with open("important.txt", "w", encoding="utf-8") as f:
pass # 什么都不写,文件已被清空
修复: 追加用 "a",覆盖前确认。
12.2 陷阱二:忘记指定 encoding
# 危险!不指定 encoding 时,Python 使用系统默认编码
# Windows 默认是 GBK/CP936,可能无法正确读取 UTF-8 的中文文件
with open("chinese.txt", "r") as f: # 可能报 UnicodeDecodeError!
content = f.read()
# 正确:始终指定 encoding
with open("chinese.txt", "r", encoding="utf-8") as f:
content = f.read()
规则: 读写包含中文(或其他非英文字符)的文件,永远要写 encoding="utf-8"。
12.3 陷阱三:捕获异常太宽泛
# 不好:捕获所有异常会隐藏 bug,很难发现真正的问题
try:
with open("data.txt") as f:
data = json.load(f)
process(data)
except Exception:
pass # 静默忽略所有错误,极难调试!
# 好:明确指定要捕获的异常类型
try:
with open("data.txt", encoding="utf-8") as f:
data = json.load(f)
process(data)
except FileNotFoundError:
print("文件不存在")
except json.JSONDecodeError:
print("文件内容不是合法 JSON")
12.4 陷阱四:在 except 里不做任何处理
# 危险!空的 except 块让错误悄无声息地消失
try:
result = 10 / 0
except ZeroDivisionError:
pass # 什么都不做?这会让 bug 很难发现
# 至少打印一下
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"警告:发生了除零错误:{e}")
result = 0 # 给一个默认值
12.5 陷阱五:误以为 finally 一定是最后执行
在 try 块里有 return 时,finally 仍然会在 return 之前执行:
def test():
try:
print("try 块")
return "try 的返回值"
finally:
print("finally 块(在 return 之前执行!)")
result = test()
print(f"返回值:{result}")
输出:
try 块
finally 块(在 return 之前执行!)
返回值:try 的返回值
第十三部分:小结
13.1 文件操作核心原则
# 始终用 with 语句打开文件
with open("文件名", "模式", encoding="utf-8") as f:
...
# 读:r(默认)
# 写(覆盖):w
# 追加:a
# 始终指定 encoding(中文文件用 utf-8)
13.2 异常处理核心原则
try:
# 可能出错的代码
...
except 具体异常类型 as e:
# 针对性处理
print(f"发生了 XX 错误:{e}")
except 另一种异常 as e:
# 另一种处理
...
else:
# try 成功时执行(可选)
...
finally:
# 无论如何都执行(可选,做清理工作)
...
13.3 文件 + 异常完整模板
def read_data(filename):
try:
with open(filename, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
print(f"文件 '{filename}' 不存在")
return None
except PermissionError:
print(f"没有权限读取 '{filename}'")
return None
except UnicodeDecodeError:
print(f"'{filename}' 不是 UTF-8 编码")
return None
def write_data(filename, content):
try:
with open(filename, "w", encoding="utf-8") as f:
f.write(content)
print(f"写入成功:{filename}")
except PermissionError:
print(f"没有权限写入 '{filename}'")
except OSError as e:
print(f"写入失败:{e}")
13.4 速查表
| 操作 | 代码 |
|---|---|
| 读取全部内容 | f.read() |
| 逐行读取(推荐) | for line in f: |
| 读取所有行为列表 | f.readlines() |
| 写入字符串 | f.write("内容n") |
| 写入多行 | f.writelines(列表) |
| 捕获文件不存在 | except FileNotFoundError: |
| 捕获编码错误 | except UnicodeDecodeError: |
| 捕获值错误 | except ValueError: |
| 捕获所有错误 | except Exception as e: |
| 主动触发异常 | raise ValueError("描述") |
| 自定义异常 | class MyError(Exception): pass |