Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 528 – 将 Windows 控制台编码更改为 UTF-8

作者:
Steve Dower <steve.dower at python.org>
状态:
最终
类型:
标准跟踪
创建:
2016年8月27日
Python 版本:
3.6
历史记录:
2016年9月1日,2016年9月4日
决议:
Python-Dev 消息

目录

摘要

历史上,Python 使用 ANSI API 与 Windows 操作系统交互,通常通过 C 运行时函数。但是,这些 API 早已不建议使用,取而代之的是 UTF-16 API。在操作系统中,所有文本都表示为 UTF-16,而 ANSI API 使用活动代码页执行编码和解码。

本 PEP 提出将 Windows 上的默认标准流实现更改为使用 Unicode API。这将允许用户在默认的 Windows 控制台中打印和输入全部范围的 Unicode 字符。这也需要对标记器如何从 readline hook 解析文本进行细微的更改。

具体更改

添加 _io.WindowsConsoleIO

目前,一个 _io.FileIO 实例用于包装表示标准输入、输出和错误的文件描述符。我们添加了一个新的类(在 C 中实现)_io.WindowsConsoleIO,它充当使用 Windows 控制台函数的原始 IO 对象,具体来说,是 ReadConsoleWWriteConsoleW

当传统模式标志未生效时,通过文件描述符打开标准流且该流是控制台缓冲区而不是重定向文件时,将使用此类。否则,将像今天一样使用 _io.FileIO

这是一个原始(字节)IO 类,需要使用 utf-8 编码传递文本,该文本将解码为 utf-16-le 并传递给 Windows API。类似地,从该类读取的字节将由操作系统作为 utf-16-le 提供,并在返回到 Python 时转换为 utf-8。

需要使用 ASCII 兼容编码来保持与绕过 TextIOWrapper 并直接将 ASCII 字节写入标准流的代码的兼容性(例如,Twisted 的 process_stdinreader.py)。假设标准流采用特定编码(而非 ASCII)的代码可能会中断。

添加 _PyOS_WindowsConsoleReadline

为了允许在交互式提示符处输入 Unicode,需要一个新的 readline hook。现有的 PyOS_StdioReadline 函数在从文件描述符(该文件描述符是控制台缓冲区且传统模式标志未生效)读取时将委托给新的 _PyOS_WindowsConsoleReadline 函数(逻辑应与上述相同)。

由于 readline 接口需要返回一个不包含嵌入空字符的 8 位编码字符串,因此 _PyOS_WindowsConsoleReadline 函数将从操作系统读取的 utf-16-le 转换为 utf-8。

目前从 sys.stdin 获取编码的函数 PyRun_InteractiveOneObject 将选择 utf-8,除非传统模式标志生效。这可能需要 readline hook 将其编码更改为 utf-8,或者需要传统模式才能确保正确行为。

添加传统模式

使用设置了环境变量 PYTHONLEGACYWINDOWSSTDIO 的方式启动 Python 将启用传统模式标志,这将完全恢复以前的行为。

替代方法

win_unicode_console 包 是一种纯 Python 替代方案,用于更改控制台的默认行为。它使用纯 Python 代码实现了此处描述的与之基本相同的修改。

可能中断的代码

以下代码模式可能会因此更改而中断或出现不同的行为。所有这些代码示例都需要明确选择使用原始文件对象来代替更方便的包装器,以防止任何可见的更改。

假设 stdin/stdout 编码

假设 sys.stdin.buffersys.stdout.buffer 所需的编码为 'mbcs' 或更具体的编码的代码可能目前碰巧能正常工作,但在此更改下可能会遇到问题。例如

>>> sys.stdout.buffer.write(text.encode('mbcs'))
>>> r = sys.stdin.buffer.read(16).decode('cp437')

要更正此代码,应使用 TextIOWrapper 上指定的编码,无论是隐式还是显式

>>> # Fix 1: Use wrapper correctly
>>> sys.stdout.write(text)
>>> r = sys.stdin.read(16)

>>> # Fix 2: Use encoding explicitly
>>> sys.stdout.buffer.write(text.encode(sys.stdout.encoding))
>>> r = sys.stdin.buffer.read(16).decode(sys.stdin.encoding)

错误地使用原始对象

使用原始 IO 对象且未正确处理部分读取和写入的代码可能会受到影响。这对于读取尤其重要,因为读取的字符数永远不会超过允许的字节数的四分之一,因为没有可行的方法可以防止输入编码为更长的 utf-8 字符串

>>> raw_stdin = sys.stdin.buffer.raw
>>> data = raw_stdin.read(15)
abcdefghijklm
b'abc'
# data contains at most 3 characters, and never more than 12 bytes
# error, as "defghijklm\r\n" is passed to the interactive prompt

要更正此代码,应使用缓冲读取器/写入器,或者调用方应继续读取,直到其缓冲区已满

>>> # Fix 1: Use the buffered reader/writer
>>> stdin = sys.stdin.buffer
>>> data = stdin.read(15)
abcedfghijklm
b'abcdefghijklm\r\n'

>>> # Fix 2: Loop until enough bytes have been read
>>> raw_stdin = sys.stdin.buffer.raw
>>> b = b''
>>> while len(b) < 15:
...     b += raw_stdin.read(15)
abcedfghijklm
b'abcdefghijklm\r\n'

使用带有小缓冲区的原始对象

使用原始 IO 对象并尝试读取少于四个字符的代码现在将收到错误。因为任何单个字符在用 utf-8 表示时都可能需要多达四个字节,所以请求必须失败

>>> raw_stdin = sys.stdin.buffer.raw
>>> data = raw_stdin.read(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: must read at least 4 bytes

唯一的解决方法是传递更大的缓冲区

>>> # Fix: Request at least four bytes
>>> raw_stdin = sys.stdin.buffer.raw
>>> data = raw_stdin.read(4)
a
b'a'
>>> >>>

(额外的 >>> 是由于换行符保留在输入缓冲区中,并且在这种情况下是预期的。)


来源:https://github.com/python/peps/blob/main/peps/pep-0528.rst

上次修改:2023年9月9日 17:39:29 GMT