Python 的 IO
IO(input/output)即输入输出。计算机最核心的功能就是获取输入来执行运算并将结果输出。其中输入和输出设备有很多,但是都被抽象为文件。在 C 语言中通常又将文件抽象为流,流可以被读取和写入。Python 中的 IO就是提供了用于操作输入和输出流相关功能的模块。
I/O 类型
IO 模块将 I/O 分为三种类型:
- 文本 I/O
- 二进制 I/O
- 原始 I/O
独立于其类型,每个具体的流对象都具有三种操作:只读、只写和可写。并且还允许任意随机访问(即向前和向后寻找任何位置)或仅仅允许顺序访问(套接字、管道都是这种情形)。
文本 I/O
文本 I/O 预期接受和输出 str 对象,这意味着他在底层的数据编码和解码都是隐式进行的,并且可以选择转换特定于平台的换行符。
创建文本流的最简单方法是使用命令:
而内存中的文本流可以通过创建 StringIO
对象来使用:
二进制 I/O
二进制 I/O(Binary I/O)也被称为缓冲区 I/O(buffered I/O)。他接受 bytes-like Objects 并输出 bytes 对象。不执行编码、解码或换行转换。此流可用于所有类型的非文本数据,也可以用于手动控制文本数据的编码和解码。
创建二进制流的最简单方法同样是并在模式字符串中指定'b'
:
对于内存中的二进制流可以通过创建 BytesIO
对象来使用:
原始 I/O
原始 I/O(Raw I/O)也称为无缓冲区 I/O(unbuffered I/O)。他是文件 I/O 和二进制 I/O 的底层构建块。用户级代码很少操作他,可以通过在禁用缓冲的情况下以二进制模式打开文件来创建原始流:
类的层次结构
I/O 流根据不同的 I/O 类型来按类的层次结构实现:
抽象基类 | 继承 | 抽象方法 | 实例方法和属性 |
---|---|---|---|
IOBase | fileno,seek,truncate | close,closed,enter,exit,flush,isatty,iter,next,readable,readline,readlines,seekable,tell,writeable,writelines | |
RawIOBase | IOBase | readinto,write | read,readall |
BufferedIOBase | IOBase | detach,read,read1,write | readinto,readinto1 |
TextIOBase | IOBase | detach,read,readline,write | encoding,errors,newlines |
其中 IOBase 是 I/O 的顶部抽象基类,其他三个也对应了不同的 I/O 类型。其中的成员含义如下:
成员 | 含义 | 特点 |
---|---|---|
close() | flush 并关闭流 | - |
closed | 如果为 True 表示流已经关闭 | - |
flush() | 将写入缓冲区刷入磁盘 | 可写流以及阻塞流适用 |
fileno() | 返回流的文件描述符(整数) | 如果 IO 对象不适用引发 OSError |
isatty() | 如果流连接到 tty 设备返回 True | - |
readable() | 如果可读返回 True,否则 False | 如果为 False read() 将引发 OSError |
readline(size=-1) | 从流中读取并返回一行,size 指定最多多少字节 | 对于二进制文件使用 b'\n' |
readlines(hint=-1) | 从流中读取并返回行列表, hint 指定返回的行数 | 对于二进制文件使用 b'\n' |
seek(offset) | 将流位置更改为给定的字偏移量 | 随机访问流 |
seekable() | 如果支持随机访问返回 True | 如果为 False seek() tell() truncate() 引发 OSError |
tell() | 返回当前流位置 | 随机访问流 |
truncate(size=None) | 将流的大小调整为 size 指定的字节数 | 可写随机访问流 |
writable() | 如果流支持写入,则返回 True | 如果 False,write() 和 truncate() 引发 OSError |
writelines(lines) | 将行序列写入流,不会添加行分隔符,如果需要换行需要在末尾拼接'\n' | 可写流 |
os.IOBase
是 Python 中所有 I/O 类的抽象基类,尽管没有定义,任何流都还需要实现:
- read(size=-1): 读取并返回最大 size 指定的字节数,如果 size 是 None 或负数则读取所有数据并返回。如果流已经到末尾则返回空 bytes 对象或空字符串
- write(byte|str): 将字符串或字节对象(具体实现)写入流并返回写入的字符数
每种抽象基类还包含一些特有的成员:
抽象基类 | 成员 | 说明 |
---|---|---|
RawIOBase | readall() | 读取所有字节 |
RawIOBase | readinto(b) | 字节输入写入 bytes 中 |
BufferedIOBase | detach() | 分离 RawIOBase 与缓冲区 |
BufferedIOBase | readinto(b) | 字节输入写入 bytes 中 |
TextIOBase | encoding | 编码 |
TextIOBase | errors | 错误设置 |
TextIOBase | newlines | 换行符 |
TextIOBase | detach() | 分离 TextIOBase 与缓冲区 |
TextIOBase | readline(size=-1) | 读取 size 行,-1 表示读取到末尾 |
TextIOBase | seek(offset) | 将流位置更改为 offset |
TextIOBase | tell() | 返回当前流位置 |
其他包装类
其他类都是继承自对应的 I/O 类型类,他们通常用于包装一个 I/O 类型来转换为另一个 I/O 类型:
io.TextIOWrapper
: 将 BufferedIOBase 包装为 TextIOBase 流io.BufferedReader
: 将 RawIOBase 包装只读的 BufferedIOBase 流io.BufferedWriter
: 将 RawIOBase 包装可写的 BufferedIOBase 流io.BytesIO(bytes=b'')
: 将内存中的字节对象包装为 BufferedIOBase 流io.StringIO(str)
: 将内存中的字符串对象包装为 TextIOBase 流
io.TextIOWrapper
继承自 TextIOBase,用于将 BufferedIOBase 包装为 TextIOBase 流:
class io.TextIOWrapper(
buffer, # BufferedIOBase
encoding=None, # 编码类型
errors=None, # 错误设置
newline=None, # 行描述符
line_buffering=False, # 是否启用行缓冲
write_through=False
)
说起来可能比较抽象,实际上就是将二进制流包装为文本流来作为管道的中间节点,例如压缩接口输出为字节流,我们没有很好的办法来循环获取行,此时就可以将这个字节流通过 TextIOWrapper 进行包装,来实现流的转换。
open()
open()
方法用于打开流,他是 Pyhton 中操作流最常用的方法,他会根据传入的参数来返回文件 I/O、二进制 I/O、原始 I/O:
open(
# 类路径对象
file,
# 打开文件模式
# 'r' - 可读(默认)
# 'w' - 可写(截断文件,即在 0 处 truncate() 表现为清空文件)
# 'x' - 独占创建(如果文件存在失败)
# 'a' - 追加
# 'b' - 二进制模式
# 't' - 文本模式(默认)
# '+' - 打开以进行更新(读取和写入)
mode='r',
# 缓冲区大小,如果 0 表示关闭仅仅 b 模式下可用
buffering=-1,
# 编码,只在 t 模式下可用
encoding=None,#
# 编码错误时如何处理,同样只在 t 模式下可用
# 'strict' - 引发 ValueError 异常(默认行为)
# 'ignore' - 忽略错误,注意可能导致数据丢失
# 'replace' - 不正确数据处使用 ? 替换
errors=None,
# 指定换行符,可能值 None '' '\n' '\n\r' '\r'
# None - 通用换行模式,可以是 '\n' '\n\r' '\r'
newline=None,
closefd=True,
opener=None
)
Tips
open 返回 file-like Object,有很多 API 接受这类对象作为参数。
I/O 流介绍
原始 I/O 是所有流的底层实现,二进制 I/O 包装原始 I/O 添加了 Buffer 来提供缓冲来提高读写性能。文本 I/O 是对二进制 I/O 的进一步包装,它在输入输出时自动编码和解码字符串。
流具有只读、只写、读写三种情形,只读只能执行 read()
类方法,他不会更改磁盘中的数据。只写只能执行 write()
方法。
流能够随机读写也有的只能顺序访问。前者具有固定的大小(例如磁盘中的文件),能够通过 seek()
来调整流位置。后者在接收到 EOF 之前并不知道具体的位置所以无法知道 size 以及调整流,例如网络传输。
Python 中的所有其他流都是在内置的三种 I/O 类型的基础上进行包装,比较著名的就是压缩流和解压流,他们都是对二进制 I/O 的包装。csv 的 Reader 和 Writer 也算是对流的包装,他包装文本 I/O 获取每一行来解析,或者将得到的数据转换为 csv 中行来写入。
Buffer
I/O 中一个比较重要的概念就是 Buffer,他被称为缓冲区。首先要了解缓冲区的物理含义类似电梯最下面的弹簧,他能够在电梯出现问题时做缓冲用的。
因此 Buffer 主要的作用是缓冲,例如要向硬盘写 100k 的数据,如果 1bit 1bit 的写入,将浪费大量的磁头移动时间,因此需要在内存中创建一个 Buffer 将 100K 写入然后统一写入到硬盘中,这样就避免冲击硬盘。读取也是同样的道理,尤其在顺序读取时大缓冲区能够极大的提高读性能。
Tips
缓冲区的核心目的是提供数据数据流缓冲,而不是内存的一个具体大小的空间,他只是实现缓冲的方式。
管道
Python 同样能够通过管道来传播流,他是实现方式与 JAVA、Nodejs 这类的完全不一样。他的流实现方式与 C 比较类似。在 Python 中所有的 IO 对象都具有 read()
和 write()
方法,他们就是能够实现流的基础,对于只读 IO 来说他只能作为流的起点,他的特点就是具有 read()
方法的对象即可,而对于可读写的 IO 来说他能够作为管道,而终点必须具有 write()
方法。
Python 中只要具备 IO 接口的都可以作为 IO 对象,他们都能够被连接,而大部分 IO 对象中传递的都是二进制对象即二进制 I/O,不过 Python 还提供了几个来实现各类 I/O 类型的封装,例如来将获取二进制流包装为文本流:
import io
import zstandard
# open rb 首先打开一个二进制流
with open("./wallstreetbets_submissions.zst", "rb") as fp:
# 解压缩包装器
dctx = zstandard.ZstdDecompressor(max_window_size=1024*1024*1024*2)
# 获取解压缩只读流,
with dctx.stream_reader(fp) as stream_reader:
# 使用文本 IO 包装二进制 IO
# 主要是为了用他的 writeline()
with io.TextIOWrapper(stream_reader, encoding="utf-8") as text_stream:
for line in text_stream:
print(line)
# 最终的效果就是: fp流 -> decompressor流 -> text流