Python 学习笔记 7:模块、包、文件操作与异常处理


一、背景

上一篇整理了 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))

模块的作用主要有三个:

  1. 复用代码
  2. 拆分代码结构
  3. 避免所有代码都堆在一个文件中

三、模块的命名空间

每个模块都有自己的命名空间。

例如:

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'

排查这类问题时,通常从下面几个方向看:

  1. 模块名是否写错
  2. 文件是否在当前项目路径中
  3. 是否少安装了第三方依赖
  4. 当前运行脚本的工作目录是否正确
  5. 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 二进制写入 创建

几个容易混淆的点:

  1. r 只读,文件不存在会报错
  2. w 会清空文件原内容
  3. a 会追加到文件末尾
  4. b 表示二进制模式,常用于图片、音频、压缩包等非文本文件
  5. 文本文件建议明确指定 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")

这里分别处理了文件不存在和编码错误。

比起直接捕获所有异常,这种写法更容易排查问题。

二十二、总结

这一篇主要整理了模块、包、文件操作和异常处理。

需要重点记住:

  1. 一个 .py 文件就是一个模块
  2. 包可以用目录组织多个模块
  3. 导入模块时,模块顶层代码会执行
  4. 测试代码建议放在 if __name__ == "__main__":
  5. 文件操作优先使用 with open(...) as f
  6. r 只读,w 覆盖写,a 追加写
  7. 异常处理优先捕获具体异常,不建议裸 except
  8. raise 可以主动抛出异常
  9. assert 适合调试检查,不适合作为唯一业务校验
  10. 自定义异常通常继承 Exception

这些内容是从脚本走向项目开发的基础,后面写面向对象、数据库操作、JSON 处理时还会继续用到。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录