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,如果对象仅支持顺序访问(例如套接字、管道和 tty),则返回 False。如果为 False,则如果调用 .seek().tell().truncate(),则会引发 IOError。

.__enter__() -> ContextManager

上下文管理协议。返回 self

.__exit__(...) -> None

上下文管理协议。与 .close() 相同。

当且仅当 RawIOBase 实现对底层文件描述符进行操作时,它还必须提供 .fileno() 成员函数。这可以通过实现专门定义,或者可以使用混合类(需要决定这一点)。

.fileno() -> int
返回底层文件描述符(一个整数)

最初,将提供三个实现来实现 RawIOBase 接口:FileIOSocketIO(在 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() 返回 Noneread() 的参数是可选的),但语义可能不同。特别是,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';所有其他值都是非法的。它控制换行符的处理方式。它的工作原理如下

  • 在输入时,如果 newlineNone,则启用通用换行符模式。输入中的行可以以 '\n''\r''\r\n' 结尾,并且在返回给调用者之前,这些换行符将转换为 '\n'。如果它是 '',则启用通用换行符模式,但换行符将未经转换地返回给调用者。如果它具有任何其他合法值,则输入行仅以给定的字符串终止,并且换行符将未经转换地返回给调用者。(换句话说,仅当 newlineNone 时才会发生转换为 '\n' 的操作。)
  • 在输出时,如果 newlineNone,则写入的任何 '\n' 字符都将转换为系统默认的行分隔符 os.linesep。如果 newline'',则不进行转换。如果 newline 为任何其他合法值,则写入的任何 '\n' 字符都将转换为给定的字符串。(请注意,指导转换的规则对于输出与输入不同。)

line_buffering 如果为 True,则如果写入的字符串至少包含一个 '\n''\r' 字符,则 write() 调用将隐含一个 flush()。当 open() 检测到底层流是 TTY 设备时,或者当传递了 1buffering 参数时,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

上次修改:2023-09-09 17:39:29 GMT