Skip to content

Python 的 IO

IO(input/output)即输入输出。计算机最核心的功能就是获取输入来执行运算并将结果输出。其中输入和输出设备有很多,但是都被抽象为文件。在 C 语言中通常又将文件抽象为流,流可以被读取和写入。Python 中的 IO就是提供了用于操作输入和输出流相关功能的模块。

I/O 类型

IO 模块将 I/O 分为三种类型:

  • 文本 I/O
  • 二进制 I/O
  • 原始 I/O

独立于其类型,每个具体的流对象都具有三种操作:只读、只写和可写。并且还允许任意随机访问(即向前和向后寻找任何位置)或仅仅允许顺序访问(套接字、管道都是这种情形)。

文本 I/O

文本 I/O 预期接受和输出 str 对象,这意味着他在底层的数据编码和解码都是隐式进行的,并且可以选择转换特定于平台的换行符。

创建文本流的最简单方法是使用命令:

Python
# 最简单的创建文本流的方式
f = open("myfile.txt", 'r', encoding='utf-8')

而内存中的文本流可以通过创建 StringIO 对象来使用:

Python
f = io.StringIO("some initial text data!")

二进制 I/O

二进制 I/O(Binary I/O)也被称为缓冲区 I/O(buffered I/O)。他接受 bytes-like Objects 并输出 bytes 对象。不执行编码、解码或换行转换。此流可用于所有类型的非文本数据,也可以用于手动控制文本数据的编码和解码。

创建二进制流的最简单方法同样是并在模式字符串中指定'b':

Python
f = open("myfile.jpg", "rb")

对于内存中的二进制流可以通过创建 BytesIO 对象来使用:

Python
f = io.BytesIO(b"some initial binary data: \x00\x01")

原始 I/O

原始 I/O(Raw I/O)也称为无缓冲区 I/O(unbuffered I/O)。他是文件 I/O 和二进制 I/O 的底层构建块。用户级代码很少操作他,可以通过在禁用缓冲的情况下以二进制模式打开文件来创建原始流:

Python
# 关闭 buffer 即意味着创建了原始I/O
f = open("myfile.jpg", "rb", buffering=0)

类的层次结构

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 流:

Python
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:

Python
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() 方法。

Text Only
read() -> write()|read() -> write()

Python 中只要具备 IO 接口的都可以作为 IO 对象,他们都能够被连接,而大部分 IO 对象中传递的都是二进制对象即二进制 I/O,不过 Python 还提供了几个来实现各类 I/O 类型的封装,例如来将获取二进制流包装为文本流:

Python
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流