PEP 3116 – 新式 I/O
- 作者:
- Daniel Stutzbach <daniel at stutzbachenterprises.com>,Guido van Rossum <guido at python.org>,Mike Verdone <mike.verdone at gmail.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建:
- 2007年2月26日
- Python 版本:
- 3.0
- 历史记录:
- 2007年2月26日
基本原理和目标
Python 允许使用各种类似流(也称为类文件)的对象,这些对象可以通过 read()
和 write()
调用来使用。任何提供 read()
和 write()
的对象都是类似流的。但是,更奇特且非常有用的函数(如 readline()
或 seek()
)可能并非在每个类似流的对象上都可用。Python 需要一个基本基于字节的 I/O 流的规范,我们可以在此基础上添加缓冲和文本处理功能。
一旦我们定义了基于字节的原始 I/O 接口,我们就可以在任何基于字节的 I/O 类之上添加缓冲和文本处理层。相同的缓冲和文本处理逻辑可用于文件、套接字、字节数组或 Python 程序员开发的自定义 I/O 类。开发流的标准定义使我们能够将基于流的操作(如 read()
和 write()
)与实现特定的操作(如 fileno()
和 isatty()
)分开。它鼓励程序员编写使用流作为流的代码,而不是要求所有流都支持特定于文件或特定于套接字的操作。
新的 I/O 规范旨在类似于 Java I/O 库,但通常不那么令人困惑。不想在新的 I/O 世界中瞎捣鼓的程序员可以预期,open()
工厂方法将生成一个与旧式文件对象向后兼容的对象。
规范
Python I/O 库将包含三层:原始 I/O 层、缓冲 I/O 层和文本 I/O 层。每一层都由一个抽象基类定义,该基类可能有多个实现。原始 I/O 和缓冲 I/O 层处理字节单位,而文本 I/O 层处理字符单位。
原始 I/O
原始 I/O 的抽象基类是 RawIOBase。它有几个方法,这些方法是围绕适当的操作系统调用的包装器。如果这些函数中的一个在对象上没有意义,则实现必须引发 IOError 异常。例如,如果以只读方式打开文件,则 .write()
方法将引发 IOError
。再举一个例子,如果对象表示套接字,则 .seek()
、.tell()
和 .truncate()
将引发 IOError
。通常,对这些函数之一的调用映射到一个操作系统调用。
.read(n: int) -> bytes
从对象中读取最多n
个字节并返回它们。如果操作系统调用返回少于n
个字节,则可能返回少于n
个字节。如果返回 0 个字节,则表示文件结束。如果对象处于非阻塞模式且没有字节可用,则调用返回None
。
.readinto(b: bytes) -> int
从对象中读取最多len(b)
个字节并将它们存储在b
中,返回读取的字节数。与 .read 类似,读取的字节数可能少于len(b)
,0 表示文件结束。如果非阻塞对象没有字节可用,则返回None
。b
的长度永远不会改变。
.write(b: bytes) -> int
返回写入的字节数,它可能< len(b)
。
.seek(pos: int, whence: int = 0) -> int
.tell() -> int
.truncate(n: int = None) -> int
.close() -> None
此外,它还定义了一些其他方法
.readable() -> bool
如果对象已打开以供读取,则返回True
,否则返回False
。如果为False
,则如果调用.read()
,则会引发IOError
。
.writable() -> bool
如果对象已打开以供写入,则返回True
,否则返回False
。如果为False
,则如果调用.write()
和.truncate()
,则会引发IOError
。
.seekable() -> bool
如果对象支持随机访问(例如磁盘文件),则返回True
,如果对象仅支持顺序访问(例如套接字、管道和 tty),则返回False
。如果为False
,则如果调用.seek()
、.tell()
和.truncate()
,则会引发 IOError。
.__enter__() -> ContextManager
上下文管理协议。返回self
。
.__exit__(...) -> None
上下文管理协议。与.close()
相同。
当且仅当 RawIOBase
实现对底层文件描述符进行操作时,它还必须提供 .fileno()
成员函数。这可以通过实现专门定义,或者可以使用混合类(需要决定这一点)。
.fileno() -> int
返回底层文件描述符(一个整数)
最初,将提供三个实现来实现 RawIOBase
接口:FileIO
、SocketIO
(在 socket 模块中)和 ByteIO
。每个实现都必须确定对象是否支持随机访问,因为用户提供的信息可能不足(考虑 open("/dev/tty", "rw")
或 open("/tmp/named-pipe", "rw")
)。例如,FileIO
可以通过调用 seek()
系统调用来确定这一点;如果它返回错误,则对象不支持随机访问。每个实现都可以提供与其类型相关的其他方法。ByteIO
对象类似于 Python 2 的 cStringIO
库,但使用新的 bytes 类型而不是字符串进行操作。
缓冲 I/O
下一层是缓冲 I/O 层,它提供对类文件对象的更有效访问。所有缓冲 I/O 实现的抽象基类是 BufferedIOBase
,它提供了与 RawIOBase 类似的方法
.read(n: int = -1) -> bytes
返回对象中的下一个n
个字节。如果到达文件末尾或对象是非阻塞的,它可能返回少于n
个字节。0 个字节表示文件结束。此方法可能多次调用RawIOBase.read()
来收集字节,或者如果所有需要的字节都已缓冲,则可能不调用RawIOBase.read()
。
.readinto(b: bytes) -> int
.write(b: bytes) -> int
将b
字节写入缓冲区。不能保证字节会立即写入原始 I/O 对象;它们可能被缓冲。返回len(b)
。
.seek(pos: int, whence: int = 0) -> int
.tell() -> int
.truncate(pos: int = None) -> int
.flush() -> None
.close() -> None
.readable() -> bool
.writable() -> bool
.seekable() -> bool
.__enter__() -> ContextManager
.__exit__(...) -> None
此外,抽象基类还提供了一个成员变量
.raw
对底层RawIOBase
对象的引用。
BufferedIOBase
方法签名大多与 RawIOBase
的签名相同(例外情况:write()
返回 None
,read()
的参数是可选的),但语义可能不同。特别是,BufferedIOBase
实现可能会读取比请求更多的数据或使用缓冲区延迟写入数据。在大多数情况下,这对用户来说是透明的(除非,例如,他们通过不同的描述符打开同一个文件)。此外,原始读取可能会返回短读取,没有任何特殊原因;缓冲读取仅在到达 EOF 时返回短读取;而原始写入可能会返回短计数(即使未启用非阻塞 I/O!),而缓冲写入在无法写入或缓冲所有字节时将引发 IOError
。
BufferedIOBase
抽象基类有四种实现,如下所述。
BufferedReader
BufferedReader
实现用于顺序访问的只读对象。它的 .flush()
方法是空操作。
BufferedWriter
BufferedWriter
实现用于顺序访问的只写对象。它的 .flush()
方法强制将所有缓存的数据写入底层的 RawIOBase
对象。
BufferedRWPair
BufferedRWPair
实现用于顺序访问的读写对象,例如套接字和 tty。由于这些对象的读写流是完全独立的,因此可以通过简单地包含一个 BufferedReader
和一个 BufferedWriter
实例来实现。它提供了一个 .flush()
方法,其语义与 BufferedWriter
的 .flush()
方法相同。
BufferedRandom
BufferedRandom
实现用于所有随机访问对象,无论它们是只读、只写还是读写。与之前对顺序访问对象进行操作的类相比,BufferedRandom
类必须处理用户调用 .seek()
来重新定位流的情况。因此,BufferedRandom
的实例必须同时跟踪对象中的逻辑位置和真实位置。它提供了一个 .flush()
方法,该方法强制将所有缓存的写入数据写入底层的 RawIOBase
对象,并将所有缓存的读取数据遗忘(以便将来的读取强制返回到磁盘)。
问:我们是否希望在规范中强制规定在读写对象上切换读取和写入意味着要进行 .flush()
操作?或者这仅仅是实现上的便利,用户不应该依赖它?
对于只读的 BufferedRandom
对象,.writable()
返回 False
,而 .write()
和 .truncate()
方法抛出 IOError
。
对于只写的 BufferedRandom
对象,.readable()
返回 False
,而 .read()
方法抛出 IOError
。
文本 I/O
文本 I/O 层提供函数用于从流中读取和写入字符串。一些新特性包括通用换行符以及字符集编码和解码。文本 I/O 层由 TextIOBase
抽象基类定义。它提供了一些类似于 BufferedIOBase
方法的方法,但它们以字符为单位而不是以字节为单位进行操作。这些方法是
.read(n: int = -1) -> str
.write(s: str) -> int
.tell() -> object
返回一个描述当前文件位置的 cookie。cookie 的唯一支持用途是与.seek()
一起使用,其中whence
设置为 0(即绝对查找)。
.seek(pos: object, whence: int = 0) -> int
查找位置pos
。如果pos
不为零,则它必须是.tell()
返回的 cookie,并且whence
必须为零。
.truncate(pos: object = None) -> int
类似于BufferedIOBase.truncate()
,除了pos
(如果非None
)必须是.tell()
之前返回的 cookie。
与原始 I/O 不同,.seek()
的单位未指定 - 一些实现(例如 StringIO
)使用字符,而其他实现(例如 TextIOWrapper
)使用字节。零的特殊情况是为了允许在没有先前的 .tell()
的情况下转到流的开头或结尾。实现可以在 .tell()
返回的 cookie 中包含流编码器状态。
TextIOBase
实现还提供了一些方法,这些方法是传递给底层 BufferedIOBase
对象的。
.flush() -> None
.close() -> None
.readable() -> bool
.writable() -> bool
.seekable() -> bool
TextIOBase
类实现还提供以下方法
.readline() -> str
读取直到换行符或 EOF 并返回该行,如果立即遇到 EOF 则返回""
。
.__iter__() -> Iterator
返回一个迭代器,该迭代器从文件中返回行(恰好是self
)。
.next() -> str
与readline()
相同,除了在立即遇到 EOF 时引发StopIteration
。
Python 库将提供两种实现。主要实现 TextIOWrapper
包装了一个 Buffered I/O 对象。每个 TextIOWrapper
对象都具有一个名为“.buffer
”的属性,该属性提供对底层 BufferedIOBase
对象的引用。它的初始化器具有以下签名
.__init__(self, buffer, encoding=None, errors=None, newline=None, line_buffering=False)
buffer
是对要使用TextIOWrapper
包装的BufferedIOBase
对象的引用。
encoding
指的是用于在字节表示和字符表示之间进行转换的编码。如果它是None
,则系统区域设置将用作默认值。
errors
是一个可选的字符串,指示错误处理。只要可以设置encoding
,就可以设置它。它默认为'strict'
。
newline
可以是None
、''
、'\n'
、'\r'
或'\r\n'
;所有其他值都是非法的。它控制换行符的处理方式。它的工作原理如下
- 在输入时,如果
newline
为None
,则启用通用换行符模式。输入中的行可以以'\n'
、'\r'
或'\r\n'
结尾,并且在返回给调用者之前,这些换行符将转换为'\n'
。如果它是''
,则启用通用换行符模式,但换行符将未经转换地返回给调用者。如果它具有任何其他合法值,则输入行仅以给定的字符串终止,并且换行符将未经转换地返回给调用者。(换句话说,仅当newline
为None
时才会发生转换为'\n'
的操作。)- 在输出时,如果
newline
为None
,则写入的任何'\n'
字符都将转换为系统默认的行分隔符os.linesep
。如果newline
为''
,则不进行转换。如果newline
为任何其他合法值,则写入的任何'\n'
字符都将转换为给定的字符串。(请注意,指导转换的规则对于输出与输入不同。)
line_buffering
如果为 True,则如果写入的字符串至少包含一个'\n'
或'\r'
字符,则write()
调用将隐含一个flush()
。当open()
检测到底层流是 TTY 设备时,或者当传递了1
的buffering
参数时,open()
会设置此参数。关于
newline
参数的更多说明
- 某些使用
'\r'
换行符生成文件的 OSX 应用程序仍然需要'\r'
支持;Excel(导出为文本时)和 Adobe Illustrator EPS 文件是最常见的示例。- 如果启用了转换,则无论调用哪种方法进行读取或写入,都会发生转换。例如,
f.read()
始终会产生与''.join(f.readlines())
相同的结果。- 如果请求在输入时不进行转换地使用通用换行符(即
newline=''
),如果系统读取操作返回的缓冲区以'\r'
结尾,则会执行另一次系统读取操作以确定它后面是否跟着'\n'
。在带有转换的通用换行符模式下,第二次系统读取操作可能会推迟到下一次读取请求,如果后续的系统读取操作返回的缓冲区以'\n'
开头,则该字符会被简单地丢弃。
另一种实现,StringIO
,创建了一个类似文件的 TextIO
实现,而没有底层的缓冲 I/O 对象。虽然可以通过将 BytesIO
对象包装在 TextIOWrapper
中来提供类似的功能,但 StringIO
对象允许更高的效率,因为它不需要实际执行编码和解码。字符串 I/O 对象可以按原样存储编码后的字符串。 StringIO
对象的 __init__
签名接受一个可选的字符串,指定初始值;初始位置始终为 0。它不支持编码或换行符转换;您始终读取回您写入的完全相同的字符。
Unicode 编码/解码问题
我们应该允许稍后更改编码和错误处理设置。在遇到 Unicode 问题和歧义(例如变音符号、代理项、编码中的无效字节)时,文本 I/O 操作的行为应该与 unicode 的 encode()
/decode()
方法相同。可能会引发 UnicodeError
。
实现说明:我们应该能够重用 codecs
模块提供的许多基础设施。如果它没有提供我们所需的精确 API,我们应该重构它以避免重复造轮子。
非阻塞 I/O
仅在原始 I/O 层面上完全支持非阻塞 I/O。如果原始对象处于非阻塞模式,并且某个操作会阻塞,则 .read()
和 .readinto()
返回 None
,而 .write()
返回 0。为了将对象置于非阻塞模式,用户必须提取 fileno 并手动执行。
在缓冲 I/O 和文本 I/O 层,如果读取或写入由于非阻塞条件而失败,它们会引发 IOError
,并将 errno
设置为 EAGAIN
。
最初,我们考虑向上传播原始 I/O 行为,但引发了许多极端情况和问题。为了解决这些问题,需要对缓冲 I/O 和文本 I/O 层进行重大更改。例如,.flush()
在缓冲的非阻塞对象上应该做什么?用户如何指示对象“尽可能多地从您的缓冲区写入,但不要阻塞”?一个不一定会刷新所有可用数据的非阻塞 .flush()
违反直觉。由于非阻塞和阻塞对象在这些层上具有如此不同的语义,因此大家一致同意放弃将它们组合成单一类型的努力。
内置函数 open()
内置函数 open()
由以下伪代码指定
def open(filename, mode="r", buffering=None, *,
encoding=None, errors=None, newline=None):
assert isinstance(filename, (str, int))
assert isinstance(mode, str)
assert buffering is None or isinstance(buffering, int)
assert encoding is None or isinstance(encoding, str)
assert newline in (None, "", "\n", "\r", "\r\n")
modes = set(mode)
if modes - set("arwb+t") or len(mode) > len(modes):
raise ValueError("invalid mode: %r" % mode)
reading = "r" in modes
writing = "w" in modes
binary = "b" in modes
appending = "a" in modes
updating = "+" in modes
text = "t" in modes or not binary
if text and binary:
raise ValueError("can't have text and binary mode at once")
if reading + writing + appending > 1:
raise ValueError("can't have read/write/append mode at once")
if not (reading or writing or appending):
raise ValueError("must have exactly one of read/write/append mode")
if binary and encoding is not None:
raise ValueError("binary modes doesn't take an encoding arg")
if binary and errors is not None:
raise ValueError("binary modes doesn't take an errors arg")
if binary and newline is not None:
raise ValueError("binary modes doesn't take a newline arg")
# XXX Need to spec the signature for FileIO()
raw = FileIO(filename, mode)
line_buffering = (buffering == 1 or buffering is None and raw.isatty())
if line_buffering or buffering is None:
buffering = 8*1024 # International standard buffer size
# XXX Try setting it to fstat().st_blksize
if buffering < 0:
raise ValueError("invalid buffering size")
if buffering == 0:
if binary:
return raw
raise ValueError("can't have unbuffered text I/O")
if updating:
buffer = BufferedRandom(raw, buffering)
elif writing or appending:
buffer = BufferedWriter(raw, buffering)
else:
assert reading
buffer = BufferedReader(raw, buffering)
if binary:
return buffer
assert text
return TextIOWrapper(buffer, encoding, errors, newline, line_buffering)
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-3116.rst