20.Python中的文件和异常

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 异常的组成

一个异常有三个关键信息:

  1. 异常类型:如 FileNotFoundErrorValueError
  2. 异常信息:具体描述错误原因
  3. 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

发表评论