Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 表示文件结束。如果非阻塞对象没有可用字节,则返回 Noneb 的长度从不更改。

.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 接口:FileIOSocketIO(在套接字模块中)和 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() 返回 Noneread() 的参数是可选的),但可能具有不同的语义。特别是,BufferedIOBase 实现可能会读取比请求更多的数据,或者使用缓冲区延迟写入数据。在大多数情况下,这对用户是透明的(除非,例如,他们通过不同的描述符打开同一个文件)。此外,原始读取可能会在没有任何特定原因的情况下返回短读取;缓冲读取只有在达到 EOF 时才会返回短读取;原始写入可能会返回短计数(即使未启用非阻塞 I/O!),而缓冲写入在无法写入或缓冲所有字节时将引发 IOError

以下是 BufferedIOBase 抽象基类的四个实现。

BufferedReader

BufferedReader 实现用于顺序访问的只读对象。它的 .flush() 方法是一个空操作。

BufferedWriter

BufferedWriter 实现用于顺序访问的只写对象。它的 .flush() 方法强制所有缓存数据写入底层 RawIOBase 对象。

BufferedRWPair

BufferedRWPair 实现用于顺序访问的读写对象,例如套接字和终端。由于这些对象的读写流完全独立,因此可以通过简单地合并 BufferedReaderBufferedWriter 实例来实现。它提供一个 .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';所有其他值都是非法的。它控制行末的处理。它的工作原理如下

  • 在输入时,如果 newlineNone,则启用通用换行模式。输入中的行可以以 '\n''\r''\r\n' 结尾,并且在返回给调用方之前,它们会被转换为 '\n'。如果为 '',则启用通用换行模式,但行尾会未翻译地返回给调用方。如果它具有任何其他合法值,则输入行仅由给定字符串终止,并且行尾会未翻译地返回给调用方。(换句话说,只有当 newlineNone 时,才会发生转换为 '\n' 的情况。)
  • 在输出时,如果 newlineNone,则写入的任何 '\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

最后修改:2025-02-01 08:59:27 GMT