CS50P / Python 笔记(七):File I/O、CSV、Lambda 与 PIL
目录
- 1. 从 List 到 File
- 2. Persistence(持久化)
- 3. File I/O
- 4. `open()`
- 5. `open()` 返回什么?
- 6. `"w"`:Write Mode
- 7. 为什么 `"w"` 有危险?
- 8. `"a"`:Append Mode
- 9. 为什么要写 `\n`?
- 10. 常见 File Mode
- 11. Text 与 Binary
- 12. 为什么需要 `close()`?
- 13. 为什么手动 `close()` 不理想?
- 14. `with`
- 15. `with` 的脑内模型
- 16. Context Manager(上下文管理器)
- 17. 读取文件
- 18. `rstrip()`
- 19. `rstrip()` 不只删除 `\n`
- 20. 不一定需要 `readlines()`
- 21. File Object 是 Iterable
- 22. `read()`、`readline()`、`readlines()`
- 23. 读取并排序名字
- 24. `sorted()` 与 `.sort()`
- 25. `sorted()` 可以直接处理 File
- 26. `reverse=True`
- 27. 从 TXT 到结构化数据
- 28. 简单 CSV
- 29. 初步解析 CSV
- 30. Unpacking(解包)
- 31. 把数据组织成 Dict
- 32. 数据格式发生了转换
- 33. 排序 Dict
- 34. `key=` 的本质
- 35. `get_name()`
- 36. 为什么是 `get_name`,不是 `get_name()`?
- 37. Function 是 First-Class Object
- 38. `sorted(..., key=...)` 的真正过程
- 39. Lambda
- 40. Lambda 的脑内模型
- 41. Lambda 不等于“更高级”
- 42. 为什么不能永远 `split(",")`?
- 43. CSV 是一种真正的数据格式
- 44. `csv.reader`
- 45. 为什么推荐 `newline=""`?
- 46. Header(表头)
- 47. `csv.DictReader`
- 48. `reader` 和 `DictReader`
- 49. DictReader 的一个重要优势
- 50. 输出时注意 f-string 引号
- 51. `csv.writer`
- 52. `csv.DictWriter`
- 53. `DictWriter` 的模型
- 54. `writeheader()`
- 55. 不要每次 append 都盲目 `writeheader()`
- 56. CSV 是文件,不是数据库
- 57. 文件格式是一种 Contract
- 58. TXT、CSV、JSON 的关系
- 59. Serialization 再次出现
- 60. Path(路径)
- 61. Encoding(字符编码)
- 62. PIL / Pillow
- 63. `Image.open()`
- 64. 命令行读取多个图片
- 65. 创建 GIF
- 66. `duration`
- 67. `loop=0`
- 68. 为什么图片也属于 File I/O?
- 69. 文件扩展名不是文件本身
- 70. 从这一章看到“抽象层”
- 71. 文件操作与操作系统
- 72. 一个完整 CSV 示例
- 73. 为什么这个结构比把所有代码写一起更好?
- 74. 数据流模型
- 75. 前几章开始真正连起来了
- 76. 常见错误
- 77. 本章知识地图
- 78. 这一章真正应该理解什么?
- 79. 从“会用文件”升级到“理解数据系统”
- 80. 一句话总结
这一章真正的主题是 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
那么这一章第一次真正让程序拥有了:
跨越运行周期保存、读取和交换数据的能力。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)