目录

这一章真正的主题是 Persistence(持久化)。

前面学习的变量、列表、字典,大部分数据都存在计算机的内存中。

程序结束之后:

内存中的数据
↓
消失

但真实的软件需要:

今天保存
↓
关闭程序
↓
明天重新打开
↓
数据还在

这就是文件存在的重要意义。


1. 从 List 到 File

例如:

names = []

for _ in range(3):
    names.append(input("What's your name? "))

for name in sorted(names):
    print(f"hello, {name}")

这里:

names

存在于内存中。

只要程序还在运行:

names
↓
["Harry", "Ron", "Hermione"]

都还存在。

但是程序结束之后:

程序结束
↓
进程结束
↓
内存被释放
↓
names 消失

如果下次重新运行程序,就必须重新输入所有名字。

这就是:

Volatile Data(易失性数据)


2. Persistence(持久化)

如果我们希望数据在程序结束之后仍然存在,就需要把它保存到某种:

Persistent Storage(持久化存储)

例如:

文本文件
CSV
JSON
数据库
图片
音频
视频
...

所以可以建立一个非常重要的模型:

Memory
↓
适合程序运行时临时处理

File / Database
↓
适合跨程序运行保存数据

3. File I/O

I/O 是:

Input / Output

File I/O 就是:

程序从文件读取数据,或者向文件写入数据。

可以理解成:

        Program
        /     \
       /       \
    Read       Write
     ↑           ↓
        File

读取:

File → Program

写入:

Program → File

4. open()

Python 内置:

open()

用于打开文件。

最常见的形式:

open(filename, mode)

例如:

file = open("names.txt", "w")

这里:

"names.txt"

是文件路径。

而:

"w"

表示:

Write Mode(写入模式)


5. open() 返回什么?

例如:

file = open("names.txt", "w")

这里:

file

不是文件内容本身。

它是一个:

File Object(文件对象)

我们可以通过这个对象与真正的文件交互。

例如:

file.write(...)
file.read(...)
file.close()

脑内模型:

Python Program
     │
     │ file object
     ↓
Operating System
     │
     ↓
names.txt

所以:

file

更像是程序操作文件的“接口”。


6. "w":Write Mode

例如:

name = input("What's your name? ")

file = open("names.txt", "w")
file.write(name)
file.close()

如果:

names.txt

不存在,通常会创建它。

如果已经存在:

原来的内容会被截断,也就是清空。

这是 "w" 最需要注意的地方。


7. 为什么 "w" 有危险?

第一次运行:

Hermione

文件:

Hermione

第二次运行:

Harry

由于:

open("names.txt", "w")

会重新以写入模式打开并清空文件。

最后文件可能只剩:

Harry

之前的:

Hermione

被覆盖了。


8. "a":Append Mode

如果我们想:

在文件末尾继续增加内容

可以使用:

"a"

即:

Append Mode(追加模式)

例如:

name = input("What's your name? ")

file = open("names.txt", "a")
file.write(f"{name}\n")
file.close()

第一次:

Hermione

第二次:

Harry

第三次:

Ron

最终:

Hermione
Harry
Ron

9. 为什么要写 \n?

如果:

file.write(name)

连续运行:

Hermione
Harry
Ron

文件可能变成:

HermioneHarryRon

因为:

write()

不像:

print()

那样自动加换行。

所以:

file.write(f"{name}\n")

显式加入:

newline

10. 常见 File Mode

以后经常会遇到:

r    read
w    write
a    append
x    exclusive creation

b    binary
t    text

+    read and write

例如:

open("data.txt", "r")

读取文本。

open("data.txt", "w")

覆盖写入文本。

open("data.txt", "a")

追加。

open("image.png", "rb")

以二进制形式读取图片。


11. Text 与 Binary

文件从更底层看,本质上都是:

bytes

但是 Python 可以提供两种主要视角:

Text Mode
↓
str

Binary Mode
↓
bytes

例如:

open("names.txt", "r")

默认是文本模式。

读到的是:

str

而:

open("image.png", "rb")

得到的主要是:

bytes

这也是为什么:

文本文件和图片文件不能完全用同一种方式理解。


12. 为什么需要 close()?

例如:

file = open("names.txt", "a")

file.write("Harry\n")

file.close()

close() 告诉 Python / 操作系统:

我已经使用完这个文件了。

这可能涉及:

刷新缓冲区
释放文件资源
释放操作系统句柄

所以打开资源之后通常应该正确关闭。


13. 为什么手动 close() 不理想?

因为程序可能变成:

file = open(...)

# 很多代码
# 很多代码
# 很多代码

file.close()

你可能忘记:

close()

甚至中途发生异常:

open
↓
写入
↓
Exception
↓
还没执行 close

因此 Python 提供了更好的资源管理方式:

with

14. with

推荐:

name = input("What's your name? ")

with open("names.txt", "a") as file:
    file.write(f"{name}\n")

注意:

with ...:

最后需要冒号。


15. with 的脑内模型

可以暂时理解成:

进入 with
↓
获取资源
↓
打开文件
↓
使用文件
↓
离开 with
↓
自动清理 / 关闭文件

所以:

with open("names.txt", "a") as file:

相当于告诉 Python:

在这个代码块中,我需要这个文件资源;离开这个代码块时,请帮我正确清理。


16. Context Manager(上下文管理器)

更正式地说:

with

使用的是:

Context Manager(上下文管理器)

它并不是只为文件存在。

它解决的是一个更普遍的问题:

获取资源
↓
使用资源
↓
无论成功还是失败
↓
正确释放资源

以后你可能在:

文件
数据库连接
锁
网络连接
事务

中继续看到:

with

所以它本质上是:

Resource Management(资源管理)机制。


17. 读取文件

例如:

with open("names.txt", "r") as file:
    lines = file.readlines()

这里:

readlines()

会读取文件中的所有行。

如果文件:

Hermione
Harry
Ron

那么:

lines

大致会得到:

[
    "Hermione\n",
    "Harry\n",
    "Ron\n",
]

注意:

换行符通常属于读取出来的字符串的一部分。


18. rstrip()

例如:

for line in lines:
    print("hello,", line.rstrip())

rstrip() 会:

删除字符串右侧的空白字符。

例如:

"Harry\n".rstrip()

得到:

"Harry"

19. rstrip() 不只删除 \n

需要注意:

rstrip()

默认删除的是:

右侧所有空白字符。

包括可能的:

空格
\t
\n
\r
...

如果我们只明确想删除换行:

line.rstrip("\n")

意图更加具体。

考虑不同平台可能存在:

\r\n

时,也常会看到:

line.rstrip("\r\n")

20. 不一定需要 readlines()

下面:

with open("names.txt") as file:
    lines = file.readlines()

for line in lines:
    ...

会先把:

所有行读进一个列表。

如果文件非常大:

10 GB

这可能很浪费内存。

实际上:

File Object 本身就是 iterable。

所以可以:

with open("names.txt") as file:
    for line in file:
        print("hello,", line.rstrip())

21. File Object 是 Iterable

这一点非常重要。

以前学习:

for student in students:

遍历 List。

现在:

for line in file:

遍历 File。

这说明:

list
file
range
dict
...

虽然是完全不同的对象,但只要它们:

支持 iteration

就可以:

for ... in ...

这就是 Python 的:

Iteration Protocol(迭代协议)

以后学习 iterator / generator 时,会深入理解这一点。


22. read()、readline()、readlines()

常见区别:

file.read()

读取大量内容,通常返回一个字符串。


file.readline()

读取一行。


file.readlines()

把多行读取成列表。


而:

for line in file:

通常是处理文本文件非常自然的方式。

脑内模型:

read()
↓
整体

readline()
↓
一行

readlines()
↓
所有行 → list

for line in file
↓
逐行迭代

23. 读取并排序名字

例如:

names = []

with open("names.txt") as file:
    for line in file:
        names.append(line.rstrip())

for name in sorted(names):
    print(f"hello, {name}")

数据流:

names.txt
↓
读取
↓
清理换行
↓
List
↓
sorted()
↓
输出

24. sorted() 与 .sort()

非常值得区分。

sorted(names)

返回:

一个新的已排序结果。

不会要求原 List 自己被修改。


而:

names.sort()

会:

原地修改 names。

和上一章:

random.shuffle(cards)

一样涉及:

Mutation(修改原对象)


25. sorted() 可以直接处理 File

因为 File 是 iterable:

with open("names.txt") as file:
    for line in sorted(file):
        print("hello,", line.rstrip())

也是合法的。

这里:

sorted(file)

会读取文件中的元素并产生排序后的结果。

不过它最终需要把待排序的数据收集起来,所以如果文件特别大:

排序本身仍然需要内存。


26. reverse=True

例如:

for name in sorted(names, reverse=True):
    print(f"hello, {name}")

默认:

reverse=False

是升序。

指定:

reverse=True

则反向排序。


27. 从 TXT 到结构化数据

假设:

names.txt

只有:

Hermione
Harry
Ron

每行只有一个信息。

但如果我们现在想保存:

名字
学院

例如:

Hermione,Gryffindor
Harry,Gryffindor
Draco,Slytherin

我们已经开始存储:

Structured Data(结构化数据)


28. 简单 CSV

CSV:

Comma-Separated Values

即:

逗号分隔值。

例如:

name,house
Hermione,Gryffindor
Harry,Gryffindor
Draco,Slytherin

可以把它理解成一张二维表:

name        house
----------------------
Hermione    Gryffindor
Harry       Gryffindor
Draco       Slytherin

29. 初步解析 CSV

例如:

with open("students.csv") as file:
    for line in file:
        row = line.rstrip().split(",")

        print(f"{row[0]} is in {row[1]}")

假设:

Hermione,Gryffindor

执行:

line.rstrip().split(",")

得到:

["Hermione", "Gryffindor"]

于是:

row[0]

是:

Hermione
row[1]

是:

Gryffindor

30. Unpacking(解包)

比:

row = line.rstrip().split(",")

name = row[0]
house = row[1]

更简洁的是:

name, house = line.rstrip().split(",")

这里:

["Hermione", "Gryffindor"]

被自动解包成:

name  → Hermione
house → Gryffindor

这和之前:

for key, value in dictionary.items():

本质上是同一种:

Unpacking(解包)

思想。


31. 把数据组织成 Dict

例如:

students = []

with open("students.csv") as file:
    for line in file:
        name, house = line.rstrip().split(",")

        student = {
            "name": name,
            "house": house,
        }

        students.append(student)

现在:

students

大致是:

[
    {
        "name": "Hermione",
        "house": "Gryffindor",
    },
    {
        "name": "Harry",
        "house": "Gryffindor",
    },
]

这就是之前学过的:

List of Dictionaries


32. 数据格式发生了转换

这里很值得观察:

File Text
↓
"Hermione,Gryffindor"

split()
↓
["Hermione", "Gryffindor"]

unpacking
↓
name / house

dict
↓
{
    "name": "Hermione",
    "house": "Gryffindor",
}

list
↓
所有学生

所以文件处理的本质经常不是:

“读字符串。”

而是:

把外部数据解析成程序内部适合处理的数据结构。


33. 排序 Dict

现在:

students

中的每一个元素不是字符串,而是:

dict

例如:

{
    "name": "Hermione",
    "house": "Gryffindor",
}

那么问题来了:

sorted() 应该按照什么排序?

按名字?

还是学院?

所以需要:

key=

34. key= 的本质

sorted() 大致可以理解:

sorted(iterable, key=None, reverse=False)

其中:

key=

要求的是:

一个函数。

这个函数负责告诉 sorted():

“对于每一个元素,我应该拿什么值作为排序依据?”


35. get_name()

例如:

def get_name(student):
    return student["name"]

然后:

for student in sorted(students, key=get_name):
    print(f"{student['name']} is in {student['house']}")

脑内模型:

每一个 student
↓
get_name(student)
↓
得到 student["name"]
↓
拿这个名字作为排序依据

36. 为什么是 get_name,不是 get_name()?

这是一个非常重要的概念。

get_name

表示:

函数对象本身。

而:

get_name()

表示:

现在立刻调用函数。


sorted() 要的是:

“把这个函数交给我,我之后会自己调用它。”

所以:

sorted(students, key=get_name)

是:

把函数交给 sorted

而不是:

sorted(students, key=get_name())

37. Function 是 First-Class Object

这里其实触碰到了 Python 一个非常重要的思想:

Functions are objects.

也就是说,函数可以:

赋值给变量
作为参数传递
作为返回值
存进数据结构

例如:

def hello():
    print("hello")


x = hello

这里:

x

也指向这个函数。

于是:

x()

仍然会调用:

hello()

这叫:

First-Class Function(一等函数)


38. sorted(..., key=...) 的真正过程

假设:

students = [
    {"name": "Ron", "house": "Gryffindor"},
    {"name": "Harry", "house": "Gryffindor"},
]

执行:

sorted(students, key=get_name)

可以粗略理解:

Ron student
↓
get_name(student)
↓
"Ron"

Harry student
↓
get_name(student)
↓
"Harry"

然后根据:
"Ron"
"Harry"

进行比较和排序

最后得到:

Harry
Ron

所以:

key 不是“比较函数”。

更加准确地说:

它是一个 key extraction function,用来从每个元素中提取排序键。


39. Lambda

前面的:

def get_name(student):
    return student["name"]

非常简单。

只用一次。

所以 Python 允许:

lambda

写一个匿名函数。

例如:

lambda student: student["name"]

等价思想:

def get_name(student):
    return student["name"]

于是:

for student in sorted(
    students,
    key=lambda student: student["name"],
):
    print(f"{student['name']} is in {student['house']}")

40. Lambda 的脑内模型

语法:

lambda parameter: expression

例如:

lambda x: x * 2

大致等价于:

def double(x):
    return x * 2

再比如:

lambda student: student["name"]

大致等价于:

def get_name(student):
    return student["name"]

41. Lambda 不等于“更高级”

如果逻辑非常简单:

lambda student: student["name"]

会很清楚。

但如果:

lambda x: ...一大堆复杂逻辑...

可读性反而可能下降。

所以:

Lambda 适合简短的一次性函数。

复杂逻辑应该优先:

def meaningful_name(...):

42. 为什么不能永远 split(",")?

这是这一章一个非常重要的升级点。

假设 CSV:

Harry,London

那么:

split(",")

没问题。

但是假设某人的家乡是:

Las Vegas, Nevada

CSV 可能正确写成:

Harry,"Las Vegas, Nevada"

这里第二个字段里面自己就包含:

,

如果你直接:

split(",")

可能错误拆成:

[
    "Harry",
    "\"Las Vegas",
    " Nevada\"",
]

这就是为什么:

CSV 不是简单的“按逗号 split”那么简单。


43. CSV 是一种真正的数据格式

CSV 需要处理:

逗号
引号
换行
转义
不同 dialect

所以 Python 标准库提供:

csv

专门处理 CSV。

核心原则:

如果已有标准解析器,不要自己重新实现格式解析器。


44. csv.reader

例如:

import csv


students = []

with open("students.csv", newline="") as file:
    reader = csv.reader(file)

    for name, home in reader:
        students.append(
            {
                "name": name,
                "home": home,
            }
        )

这里:

csv.reader(file)

负责正确解析 CSV。

于是即使:

Harry,"Las Vegas, Nevada"

它也知道:

"Las Vegas, Nevada"

属于一个字段。


45. 为什么推荐 newline=""?

Python 官方 csv 模块通常推荐:

open("students.csv", newline="")

让:

csv

模块自己正确处理 CSV 中的换行。

特别是在:

跨平台读写 CSV

时更加稳妥。

因此可以养成:

with open("students.csv", newline="") as file:

的习惯。


46. Header(表头)

更加正规的 CSV 经常第一行是:

name,home
Hermione,England
Harry,"Godric's Hollow"

第一行:

name,home

描述的是字段名称。

叫:

Header(表头)

这时候使用:

csv.DictReader

就非常方便。


47. csv.DictReader

例如:

import csv


students = []

with open("students.csv", newline="") as file:
    reader = csv.DictReader(file)

    for row in reader:
        students.append(
            {
                "name": row["name"],
                "home": row["home"],
            }
        )

假设:

name,home
Hermione,England
Harry,Godric's Hollow

第一轮:

row

大致是:

{
    "name": "Hermione",
    "home": "England",
}

第二轮:

{
    "name": "Harry",
    "home": "Godric's Hollow",
}

48. reader 和 DictReader

普通:

csv.reader

每行主要类似:

["Hermione", "England"]

所以需要:

name, home = row

而:

csv.DictReader

每行主要类似:

{
    "name": "Hermione",
    "home": "England",
}

所以:

row["name"]
row["home"]

可以记:

csv.reader
↓
List-like rows

csv.DictReader
↓
Dict-like rows

49. DictReader 的一个重要优势

假设原来 CSV:

name,home

后来变成:

home,name

如果使用:

row[0]
row[1]

代码含义可能完全变化。

但如果:

row["name"]
row["home"]

只要 Header 正确:

字段位置变化并不会让代码语义变得模糊。

所以使用字段名是一个:

Schema-aware(感知数据结构)

的思路。


50. 输出时注意 f-string 引号

例如:

print(f"{student["name"]} is from {student["home"]}")

在一些 Python 版本/环境中这样的引号嵌套会产生语法问题,学习阶段建议写得更清晰:

print(f"{student['name']} is from {student['home']}")

即:

外层 "
内层 '

这样最不容易混乱。


51. csv.writer

向 CSV 写数据:

import csv


name = input("What's your name? ")
home = input("Where's your home? ")


with open("students.csv", "a", newline="") as file:
    writer = csv.writer(file)

    writer.writerow([name, home])

这里:

writer.writerow(...)

负责:

将一行数据正确编码成 CSV。

如果:

home = Las Vegas, Nevada

CSV writer 会负责正确引用。

这比:

file.write(f"{name},{home}\n")

更加可靠。


52. csv.DictWriter

如果希望用:

field name

而不是位置组织数据:

import csv


name = input("What's your name? ")
home = input("Where's your home? ")


with open("students.csv", "a", newline="") as file:
    writer = csv.DictWriter(
        file,
        fieldnames=["name", "home"],
    )

    writer.writerow(
        {
            "name": name,
            "home": home,
        }
    )

注意参数名是:

fieldnames

不是:

filenames

53. DictWriter 的模型

这里先告诉 writer:

fieldnames=["name", "home"]

也就是:

CSV 有两个字段:

name
home

然后:

writer.writerow(
    {
        "name": name,
        "home": home,
    }
)

就是:

Dict
↓
根据 fieldnames 顺序
↓
CSV row

54. writeheader()

如果我们创建一个新的 CSV:

import csv


with open("students.csv", "w", newline="") as file:
    writer = csv.DictWriter(
        file,
        fieldnames=["name", "home"],
    )

    writer.writeheader()

    writer.writerow(
        {
            "name": "Harry",
            "home": "Godric's Hollow",
        }
    )

结果:

name,home
Harry,Godric's Hollow

其中:

writeheader()

负责写:

name,home

55. 不要每次 append 都盲目 writeheader()

如果每次:

open(..., "a")

之后都:

writer.writeheader()

文件可能变成:

name,home
Harry,London
name,home
Ron,England
name,home
Hermione,England

所以 Header 通常:

创建文件时写一次。

这是文件生命周期设计的一部分。


56. CSV 是文件,不是数据库

这一点非常值得提前理解。

CSV 可以存储结构化数据:

name
home
score
...

但它仍然主要是:

一个文本文件格式。

它没有天然提供数据库常见能力:

复杂查询
索引
事务
并发控制
关系约束
高效更新
权限管理

所以:

CSV

非常适合:

数据交换
简单数据保存
导入导出
分析

但数据规模和系统复杂度上升以后,通常会学习:

SQLite
PostgreSQL
MySQL
...

57. 文件格式是一种 Contract

例如 CSV:

name,home
Harry,London

实际上定义了一种:

数据交换契约。

程序 A 写:

name,home

程序 B 知道:

第一列 / name
第二列 / home

于是两个程序即使完全独立开发,也可以交换数据。

这和前面学习:

JSON
API

是同一种更高层思想:

程序通过约定好的数据格式沟通。


58. TXT、CSV、JSON 的关系

可以建立一个模型:

TXT
↓
主要只是文本
结构通常由程序员自己定义

CSV
↓
表格型结构
row / column

JSON
↓
层级结构
object / array / value

例如:

TXT
Harry
Ron

CSV
name,house
Harry,Gryffindor

JSON
{
  "name": "Harry",
  "house": "Gryffindor"
}

本质上:

都是在把内存中的数据转换成可以持久保存或交换的外部表示。


59. Serialization 再次出现

上一章学习 JSON 时出现:

Serialization(序列化)

其实 CSV 也可以放进这个大模型:

Python Object
↓
编码 / Serialization
↓
File Format
↓
Disk

读取则是:

Disk
↓
File Format
↓
Parsing / Deserialization
↓
Python Object

所以 File I/O 不只是:

“打开文件。”

更大的主题是:

内存中的对象如何转换为外部持久表示,再恢复回来。


60. Path(路径)

目前:

open("names.txt")

使用的是:

相对路径。

意思大致是:

从当前工作目录寻找 names.txt。

以后会经常遇到:

data/
students.csv

于是:

open("data/students.csv")

更现代的 Python 项目也经常使用:

pathlib

例如:

from pathlib import Path


path = Path("data") / "students.csv"

这是以后非常值得学习的文件路径工具。


61. Encoding(字符编码)

文本文件中的:

Harry
你好
こんにちは

最终都需要变成:

bytes

才能保存到磁盘。

这中间就涉及:

Character Encoding(字符编码)

现在最常见的是:

UTF-8

所以实际项目中经常会明确:

with open(
    "names.txt",
    "r",
    encoding="utf-8",
) as file:
    ...

尤其涉及:

中文
日文
Emoji
跨平台文件

时,这个概念非常重要。


62. PIL / Pillow

这一章最后开始处理:

图片文件。

Python 常用:

Pillow

这个第三方库处理图像。

安装名:

Pillow

但代码中通常:

from PIL import Image

这再次体现上一章提到:

安装包的名字和 import namespace 不一定一样。


63. Image.open()

例如:

from PIL import Image


image = Image.open("costume1.gif")

这里:

Image.open(...)

会打开图像。

返回的:

image

是一个:

Image Object

然后可以:

读取尺寸
改变大小
裁剪
旋转
过滤
保存
...

64. 命令行读取多个图片

例如:

import sys

from PIL import Image


images = []

for arg in sys.argv[1:]:
    image = Image.open(arg)
    images.append(image)

假设运行:

python costumes.py costume1.gif costume2.gif

那么:

sys.argv[1:]

大致:

[
    "costume1.gif",
    "costume2.gif",
]

于是:

文件路径
↓
Image.open()
↓
Image Object
↓
images list

65. 创建 GIF

例如:

images[0].save(
    "costumes.gif",
    save_all=True,
    append_images=images[1:],
    duration=200,
    loop=0,
)

这里:

images[0]

作为第一帧。

append_images=images[1:]

把剩余图片作为后续帧。

注意如果图片数量可能超过两个:

images[1:]

比:

[images[1]]

更加通用。


66. duration

duration=200

表示每帧显示时间。

通常以毫秒为单位。

例如:

200 ms

即:

0.2 秒

67. loop=0

对于 GIF:

loop=0

通常表示:

不断循环播放。

于是:

Image 1
↓
Image 2
↓
Image 1
↓
Image 2
↓
...

形成动画。


68. 为什么图片也属于 File I/O?

因为:

TXT
CSV
JSON
PNG
JPEG
GIF

从操作系统角度看:

都是文件。

区别主要在于:

文件里的 bytes

应该按照什么格式解释。

例如:

TXT
↓
文本编码解释

CSV
↓
CSV parser

JSON
↓
JSON parser

JPEG
↓
JPEG decoder

GIF
↓
GIF decoder

所以:

File I/O 是底层共同机制,File Format 决定数据应该如何被解释。


69. 文件扩展名不是文件本身

例如:

picture.jpg

中的:

.jpg

只是一个文件名约定。

真正决定文件内容的是:

文件中的二进制数据以及对应格式。

简单改名:

image.jpg
↓
image.txt

并不会把 JPEG 真正转换成文本。

这一点以后处理:

MIME type
编码
文件格式
网络传输

时会非常重要。


70. 从这一章看到“抽象层”

这一章表面上代码是:

open(...)
csv.reader(...)
Image.open(...)

但背后已经可以看到多个抽象层:

Application Logic
↓
Python API
↓
File Object
↓
Operating System
↓
File System
↓
Storage Device

例如:

file.write("Harry")

你不需要自己:

寻找磁盘扇区
控制 SSD
管理文件系统 metadata

这些全部被下层抽象掉了。

这就是:

Abstraction(抽象)


71. 文件操作与操作系统

事实上:

open()

并不是 Python 凭空创造一个文件。

大致可以理解:

Python
↓
请求 Operating System
↓
Operating System
↓
File System
↓
打开 / 创建文件
↓
返回资源
↓
Python File Object

所以文件本身属于:

Operating System Resource

这也是为什么:

open
close
with

涉及资源生命周期。


72. 一个完整 CSV 示例

例如:

import csv


def main():
    students = load_students("students.csv")

    for student in sorted(
        students,
        key=lambda student: student["name"],
    ):
        print(
            f"{student['name']} is from "
            f"{student['home']}"
        )


def load_students(filename):
    students = []

    with open(
        filename,
        newline="",
        encoding="utf-8",
    ) as file:
        reader = csv.DictReader(file)

        for row in reader:
            students.append(
                {
                    "name": row["name"],
                    "home": row["home"],
                }
            )

    return students


if __name__ == "__main__":
    main()

这里已经把:

读取数据

封装成:

load_students()

而:

main()

负责程序主要逻辑。


73. 为什么这个结构比把所有代码写一起更好?

因为现在职责变成:

main()
↓
程序流程

load_students()
↓
负责读取并解析文件

csv.DictReader
↓
负责 CSV 语法

open()
↓
负责文件资源

不同层各自负责不同事情。

这又回到了之前学习的软件设计思想:

Separation of Concerns(关注点分离)


74. 数据流模型

这一章最值得建立的是:

Disk
↓
File
↓
open()
↓
File Object
↓
Parser
↓
Python Data Structure
↓
Business Logic
↓
Python Data Structure
↓
Serializer / Writer
↓
File
↓
Disk

例如 CSV:

students.csv
↓
open()
↓
csv.DictReader
↓
dict
↓
list
↓
排序 / 过滤 / 计算
↓
csv.DictWriter
↓
students.csv

75. 前几章开始真正连起来了

现在可以看到:

Variables
↓
保存单个运行时状态

List / Dict
↓
组织内存中的数据

Loops
↓
批量处理数据

Functions
↓
封装数据处理逻辑

Exceptions
↓
处理读取 / 写入失败

Modules
↓
组织文件处理代码

Files
↓
持久化数据

CSV / JSON
↓
结构化数据交换

Testing
↓
验证文件处理逻辑

知识不再是独立章节。

它们开始组合成真正的程序。


76. 常见错误

忘记 with 后面的冒号

错误:

with open("names.txt") as file

正确:

with open("names.txt") as file:

使用 "w" 意外覆盖文件

open("names.txt", "w")

会清空原文件内容。

需要追加时:

open("names.txt", "a")

忘记换行

file.write(name)

可能导致:

HarryRonHermione

通常:

file.write(f"{name}\n")

错把 CSV 当成简单字符串

line.split(",")

无法正确处理所有 CSV。

复杂或真实 CSV 应该使用:

csv.reader

或:

csv.DictReader

DictWriter 参数拼错

正确:

fieldnames=

不是:

filenames=

key=get_name() 写错

错误:

sorted(students, key=get_name())

正确:

sorted(students, key=get_name)

因为:

key=

需要函数本身。


f-string 引号混乱

推荐:

print(
    f"{student['name']} is from {student['home']}"
)

77. 本章知识地图

File I/O
│
├── Persistence
│   ├── memory
│   └── disk
│
├── open()
│   ├── file path
│   ├── file mode
│   └── file object
│
├── Modes
│   ├── r
│   ├── w
│   ├── a
│   ├── x
│   ├── b
│   └── +
│
├── Resource Management
│   ├── close()
│   ├── with
│   └── context manager
│
├── Reading
│   ├── read()
│   ├── readline()
│   ├── readlines()
│   └── for line in file
│
├── Text Processing
│   ├── rstrip()
│   ├── split()
│   └── unpacking
│
├── Sorting
│   ├── sorted()
│   ├── reverse=
│   ├── key=
│   ├── function object
│   └── lambda
│
├── CSV
│   ├── reader
│   ├── DictReader
│   ├── writer
│   ├── DictWriter
│   ├── fieldnames
│   └── writeheader()
│
├── Data Representation
│   ├── TXT
│   ├── CSV
│   ├── JSON
│   └── Serialization
│
├── File System
│   ├── relative path
│   ├── pathlib
│   └── encoding
│
└── Binary Files
    └── Pillow / PIL
        ├── Image.open()
        └── Image.save()

78. 这一章真正应该理解什么?

表面上学的是:

open
with
readlines
write
sorted
lambda
csv
PIL

但真正应该理解的是几个更大的概念。


第一层:Persistence

RAM
↓
程序结束就消失

Disk
↓
程序结束之后仍存在

第二层:Resource Management

open
↓
use
↓
close

而:

with

帮助我们安全管理资源生命周期。


第三层:Parsing

磁盘上的数据:

Hermione,Gryffindor

对于 Python 来说最开始只是:

text

我们需要:

parse
↓
Python objects

第四层:Serialization

反方向:

Python objects
↓
serialize
↓
external format
↓
disk / network

第五层:Abstraction

你调用:

csv.DictReader

不用自己处理:

逗号
双引号
转义
换行

因为这些复杂性被:

csv module

抽象掉了。


79. 从“会用文件”升级到“理解数据系统”

初级理解:

open() 可以读文件。

再进一步:

File Object 是 Python 与操作系统文件资源之间的接口。

再进一步:

文件只是持久化 byte sequence,文件格式决定 bytes 如何被解释。

再进一步:

CSV / JSON 是数据表示协议。

再进一步:

程序的核心工作之一,就是让数据在不同表示之间流动和转换。

例如:

Disk Bytes
↓
UTF-8
↓
Text
↓
CSV Parser
↓
Dict
↓
Business Logic
↓
Dict
↓
CSV Writer
↓
Text
↓
Bytes
↓
Disk

80. 一句话总结

这一章表面上是在学习:

open()
with
read()
write()
CSV
lambda
PIL

真正学习的是:

程序如何让数据离开短暂的内存,进入持久化存储;以及如何在“文件格式”和“Python 对象”之间进行转换。

如果把程序看成一个数据加工系统:

External Data
     ↓
   Input
     ↓
 Parsing
     ↓
Python Objects
     ↓
 Processing
     ↓
Python Objects
     ↓
Serialization
     ↓
  Output
     ↓
External Data

那么这一章第一次真正让程序拥有了:

跨越运行周期保存、读取和交换数据的能力。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐