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
;如果对象仅支持顺序访问(例如套接字、管道和终端),则返回False
。如果为False
,则调用.seek()
、.tell()
和.truncate()
将引发 IOError。
.__enter__() -> ContextManager
上下文管理协议。返回self
。
.__exit__(...) -> None
上下文管理协议。与.close()
相同。
当且仅当 RawIOBase
实现操作底层文件描述符时,它必须额外提供 .fileno()
成员函数。这可以由实现专门定义,也可以使用混合类(需要对此做出决定)。
.fileno() -> int
返回底层文件描述符(一个整数)
最初,将提供三个实现来满足 RawIOBase
接口:FileIO
、SocketIO
(在套接字模块中)和 ByteIO
。每个实现都必须确定对象是否支持随机访问,因为用户提供的信息可能不足(考虑 open("/dev/tty", "rw")
或 open("/tmp/named-pipe", "rw")
)。例如,FileIO
可以通过调用 seek()
系统调用来确定这一点;如果它返回错误,则对象不支持随机访问。每个实现可以提供适合其类型的额外方法。ByteIO
对象类似于 Python 2 的 cStringIO
库,但操作的是新字节类型而不是字符串。
缓冲 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
实现用于顺序访问的读写对象,例如套接字和终端。由于这些对象的读写流完全独立,因此可以通过简单地合并 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
读取直到换行符或文件结束,并返回该行,如果立即遇到文件结束则返回""
。
.__iter__() -> Iterator
返回一个迭代器,该迭代器从文件中返回行(恰好是self
)。
.next() -> str
与readline()
相同,但如果立即遇到 EOF 则引发StopIteration
。
Python 库将提供两个实现。主要实现 TextIOWrapper
包装一个缓冲 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 设备时,或者当传递buffering
参数为1
时,就会设置此项。关于
newline
参数的进一步说明
- 对于某些使用
'\r'
行尾符生成文件的 OSX 应用程序,仍然需要'\r'
支持;Excel(导出到文本时)和 Adobe Illustrator EPS 文件是最常见的示例。- 如果启用了翻译,无论调用哪个方法进行读取或写入,它都会发生。例如,
f.read()
总是会产生与''.join(f.readlines())
相同的结果。- 如果在输入时请求不带翻译的通用换行符(即
newline=''
),如果系统读取操作返回以'\r'
结尾的缓冲区,则会执行另一个系统读取操作以确定其后是否跟有'\n'
。在带翻译的通用换行模式下,第二次系统读取操作可能会推迟到下一次读取请求,如果后续系统读取操作返回以'\n'
开头的缓冲区,则该字符将被简单地丢弃。
另一个实现 StringIO
创建一个类似文件的 TextIO
实现,它没有底层缓冲 I/O 对象。虽然通过将 BytesIO
对象包装在 TextIOWrapper
中可以提供类似的功能,但 StringIO
对象可以提供更高的效率,因为它不需要实际执行编码和解码。String 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