PEP 529 – 将 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 上的默认文件系统编码更改为 utf-8,并将所有文件系统函数更改为使用 Unicode API 处理文件系统路径。这将不会影响使用字符串表示路径的代码,但使用字节表示路径的代码现在将能够正确地往返所有 Windows 文件系统中的有效路径。目前,Unicode(在操作系统中)和字节(在 Python 中)之间的转换是有损的,并且无法往返用户活动代码页之外的字符。
值得注意的是,这不会影响文件内容的编码。这些将继续默认为 locale.getpreferredencoding()
(对于文本文件)或纯字节(对于二进制文件)。这仅影响用户将字节对象传递给 Python(然后将其作为路径名传递给操作系统)时使用的编码。
背景
文件系统路径几乎普遍表示为文本,其编码由文件系统决定。在 Python 中,我们通过许多接口公开这些路径,例如 os
和 io
模块。路径可以在这些接口之间双向传递,即从文件系统到应用程序(例如,os.listdir()
),或从应用程序到文件系统(例如,os.unlink()
)。
当路径在文件系统和应用程序之间传递时,它们要么作为字节块传递,要么使用 os.fsencode()
和 os.fsdecode()
转换为/从 str,或者使用 sys.getfilesystemencoding()
进行显式编码。使用 sys.getfilesystemencoding()
编码字符串的结果是默认文件系统的本机格式的字节块。
在 Windows 上,文件系统的本机格式是 utf-16-le。所有用于访问文件系统的推荐平台 API 都接受并返回以此格式编码的文本。然而,在 Windows NT 之前(可能更早),本机格式是可配置的机器选项,并且存在一组单独的 API 来接受此格式。该选项(“活动代码页”)和这些 API(“*A 函数”)在最新版本的 Windows 中仍然存在,以实现向后兼容性,尽管新功能通常只有 utf-16-le API(“*W 函数”)。
在 Python 中,推荐使用 str,因为它能够正确地往返路径中使用的所有字符(在 POSIX 上使用 surrogateescape 处理;在 Windows 上因为 str 映射到本机表示)。在 Windows 上,字节无法往返路径中使用的所有字符,因为 Python 内部使用 *A 函数,因此编码是“无论活动代码页是什么”。由于活动代码页无法表示所有 Unicode 字符,因此将路径转换为字节可能会丢失信息,而不会有警告或任何可用指示。
作为此的演示
>>> open('test\uAB00.txt', 'wb').close()
>>> import glob
>>> glob.glob('test*')
['test\uab00.txt']
>>> glob.glob(b'test*')
[b'test?.txt']
对 glob 的第二次调用中的 Unicode 字符已被“?”替换,这意味着将路径传回文件系统将导致 FileNotFoundError
。使用 os.listdir()
或任何匹配返回类型与参数类型的函数可能会观察到相同的结果。
虽然用户可访问的修复是 everywhere 使用 str,但 POSIX 系统通常在使用字节时不会出现数据丢失,因为字节是规范表示。即使编码根据某些标准是“不正确的”,文件系统仍然会将字节映射回文件。利用这一点可以避免解码和重新编码的成本,因此(理论上,并且仅在 POSIX 上),像这样的代码可能会更快,因为使用了 b'.'
而不是 '.'
>>> for f in os.listdir(b'.'):
... os.stat(f)
...
因此,以 POSIX 为中心的库作者更喜欢使用字节来表示路径。对于某些作者来说,这也是一种便利,因为他们的代码可能收到已知已正确编码的字节,而另一些作者则试图简化将代码从 Python 2 移植。然而,正确性假设不适用于将 Unicode 作为规范表示的 Windows,并且可能会导致错误。这种潜在的数据丢失是 Python 3.3 中弃用 Windows 上字节路径的原因——所有上述代码片段在 Windows 上都会产生弃用警告。
提案
目前默认的文件系统编码是“mbcs”,这是一个使用活动代码页的元编码器。然而,当字节传递给文件系统时,它们通过 *A API,操作系统处理编码。在这种情况下,路径总是使用等同于“mbcs:replace”进行编码,Python 无法覆盖或更改此编码。
此提案将删除所有 *A API 的使用,并且只调用 *W API。当 Windows 将路径作为 str
返回给 Python 时,它们将从 utf-16-le 解码并作为文本返回(无论最小表示是什么)。当 Python 代码请求路径作为 bytes
时,路径将使用 surrogatepass 从 utf-16-le 转换为 utf-8(Windows 不验证代理对,因此文件名中可能存在无效的代理)。同样,当路径作为 bytes
提供时,它们将从 utf-8 转换为 utf-16-le 并传递给 *W API。
utf-8 的使用将不可配置,除非提供“旧版模式”标志以恢复到以前的行为。
surrogateescape
错误模式不适用于此处,因为关注点不在于保留无意义的字节。操作系统返回的任何路径都将是有效的 Unicode,而用户创建的无效路径应引发解码错误(目前这些将引发 OSError
或其子类)。
选择 utf-8 字节(而不是 utf-16-le 字节)是为了确保能够往返路径名,并允许在假定 ASCII 兼容编码时进行基本操作(例如,使用 os.path
模块)。使用 utf-16-le 作为编码更纯粹,但会导致更多问题而非解决问题。
此更改还将取消对 Windows 上字节路径使用的弃用。使用字节作为路径的语义无需更改——与以前一样,它们必须使用 sys.getfilesystemencoding()
指定的编码进行编码。
具体更改
更新 sys.getfilesystemencoding
删除 Py_FileSystemDefaultEncoding
的默认值,并在 initfsencoding()
中将其设置为 utf-8,如果启用旧版模式开关,则设置为 mbcs。
更新 PyUnicode_DecodeFSDefaultAndSize()
和 PyUnicode_EncodeFSDefault()
的实现以使用 utf-8 编码器,如果启用旧版模式开关,则使用现有的 mbcs 编码器。
添加 sys.getfilesystemencodeerrors
由于错误模式现在可以在 surrogatepass
和 replace
之间切换,手动执行编码的 Python 代码也需要访问当前错误模式。这包括 os.fsencode()
和 os.fsdecode()
的实现,它们目前根据编解码器假设错误模式。
添加一个公共的 Py_FileSystemDefaultEncodeErrors
,类似于现有的 Py_FileSystemDefaultEncoding
。Windows 上的默认值将是 surrogatepass
,或在旧版模式下为 replace
。所有其他平台上的默认值将是 surrogateescape
。
添加一个公共的 sys.getfilesystemencodeerrors()
函数,返回当前错误模式。
更新 PyUnicode_DecodeFSDefaultAndSize()
和 PyUnicode_EncodeFSDefault()
的实现,以使用变量作为错误模式而不是常量字符串。
更新 os.fsencode()
和 os.fsdecode()
的实现,以使用 sys.getfilesystemencodeerrors()
而不是假设模式。
更新 path_converter
更新路径转换器,始终使用 PyUnicode_DecodeFSDefaultAndSize()
将字节或缓冲区对象解码为文本。
将 narrow
字段从 char*
字符串更改为指示原始对象是否为字节的标志。这对于需要使用与原始提供类型相同的类型返回路径的函数是必需的。
删除未使用的 ANSI 代码
删除所有使用 narrow
字段的代码路径,因为这些将不再被任何调用者访问。这些仅在 posixmodule.c
中使用。路径的其他使用应将字节路径替换为解码和使用 *W API。
添加旧版模式
添加一个旧版模式标志,通过环境变量 PYTHONLEGACYWINDOWSFSENCODING
或通过函数调用 sys._enablelegacywindowsfsencoding()
启用。函数调用只能用于启用该标志,并且程序应尽可能在初始化附近使用。Python 运行时无法禁用旧版模式。
当此标志设置时,默认文件系统编码设置为 mbcs 而不是 utf-8,错误模式设置为 replace
而不是 surrogatepass
。路径将继续解码为宽字符,并且只调用 *W API,但是,Python 传入和接收的字节将与此更改之前相同的方式编码。
取消对 Windows 上字节路径的弃用
在 Windows 上使用字节作为路径目前已被弃用。我们将宣布不再如此,并且当路径编码为字节时,应使用 sys.getfilesystemencoding()
返回的值,而不是用户的活动代码页。
Beta 实验
为了帮助确定此更改的影响,我们建议将其临时应用于 3.6.0b1,并打算在 3.6.0b4 之前做出最终决定。
在实验期间,解码和编码异常消息将扩展为包含指向活动在线讨论的链接,并鼓励报告问题。
如果决定在 3.6.0b4 中恢复此功能,则实现更改将是永久启用旧版模式标志,将环境变量更改为 PYTHONWINDOWSUTF8FSENCODING
,并将函数更改为 sys._enablewindowsutf8fsencoding()
,以允许在个案基础上启用该功能,而不是禁用它。
预计如果我们由于兼容性问题而无法在 3.6 中实际进行更改,那么在 Python 3.x 后期的任何时候都将无法进行更改。
受影响的模块
本 PEP 隐含地包括 Python 中所有将路径名传递给操作系统或以其他方式使用 sys.getfilesystemencoding()
的模块。
截至 3.6.0a4,以下模块需要修改
os
_overlapped
_socket
subprocess
zipimport
以下模块使用 sys.getfilesystemencoding()
但不需要修改
gc
(已经假设字节是 utf-8)grp
(未针对 Windows 编译)http.server
(正确地在传输数据中包含编码器名称)idlelib.editor
(不应需要;有备用处理)nis
(未针对 Windows 编译)pwd
(未针对 Windows 编译)spwd
(未针对 Windows 编译)_ssl
(仅用于 ASCII 常量)tarfile
(Windows 上未使用的代码)_tkinter
(已经假设字节是 utf-8)wsgiref
(被假定为未知环境的默认编码)zipapp
(Windows 上未使用的代码)
以下本机代码使用其中一个编码或解码函数,但不需要任何修改
Parser/parsetok.c
(文档已指定sys.getfilesystemencoding()
)Python/ast.c
(文档已指定sys.getfilesystemencoding()
)Python/compile.c
(未文档化,但隐含 Python 文件系统编码)Python/errors.c
(文档已指定os.fsdecode()
)Python/fileutils.c
(Windows 上未使用的代码)Python/future.c
(未文档化,但隐含 Python 文件系统编码)Python/import.c
(文档已指定 utf-8)Python/importdl.c
(Windows 上未使用的代码)Python/pythonrun.c
(文档已指定sys.getfilesystemencoding()
)Python/symtable.c
(未文档化,但隐含 Python 文件系统编码)Python/thread.c
(Windows 上未使用的代码)Python/traceback.c
(正确编码以比较字符串)Python/_warnings.c
(文档已指定os.fsdecode()
)
被拒绝的替代方案
使用严格的 mbcs 解码
这与提议的更改本质上相同,但不是将 sys.getfilesystemencoding()
更改为 utf-8,而是将其更改为 mbcs(它动态映射到活动代码页)。
这种方法允许使用仅作为 *W API 可用的新功能,以及检测编码/解码错误。例如,不是静默地将 Unicode 字符替换为“?”,而是可以警告或失败操作。
与提议的修复相比,这可以启用一些新功能,但没有解决最初描述的任何问题。新的运行时错误可能会使一些问题更加明显并导致修复,前提是库维护者有兴趣支持 Windows 并添加单独的代码路径将文件系统路径视为字符串。
在没有严格错误的情况下将编码设置为 mbcs 等同于默认启用旧版模式开关。如果实际代码出现重大中断并且需要延长弃用期,但仍希望简化 CPython 源代码,这是一种可能的行动方案。
使 Windows 上的字节路径成为错误
通过完全阻止在 Windows 上使用字节路径,我们可以防止用户遇到编码问题。
然而,本 PEP 的动机是增加在 POSIX 上编写的代码也能在 Windows 上正确工作的可能性。这种替代方案将朝着相反的方向发展,使此类代码完全不兼容。由于这对用户没有任何好处,我们拒绝它。
使所有平台上的字节路径成为错误
通过弃用并禁用所有平台上的字节路径,无论代码最初在哪里编写,我们都可以防止用户遇到编码问题。这将需要一个完整的弃用周期,因为目前除 Windows 外的平台上没有警告。
这很可能被视为对 Python 开发人员的普遍敌对行为,因此此时被拒绝。
可能导致代码中断
以下代码模式可能会因此次更改而中断或出现不同的行为。这些示例中的每一个在旨在跨平台使用的代码中都会很脆弱。建议的修复演示了在所有平台和多个 Python 版本中处理路径编码问题的最兼容方式。
请注意,所有这些示例在 Python 3.3 及更高版本中都会产生弃用警告。
未跨边界管理编码
跨协议边界时不管理编码的代码目前可能偶然工作,但当任一编码更改时可能会遇到问题。请注意,filename
的来源可以是任何返回字节对象的函数,如下面的第二个示例所示
>>> filename = open('filename_in_mbcs.txt', 'rb').read()
>>> text = open(filename, 'r').read()
为了更正此代码,应指定 filename
中字节的编码,无论是在从文件读取时还是在使用值之前
>>> # Fix 1: Open file as text (default encoding)
>>> filename = open('filename_in_mbcs.txt', 'r').read()
>>> text = open(filename, 'r').read()
>>> # Fix 2: Open file as text (explicit encoding)
>>> filename = open('filename_in_mbcs.txt', 'r', encoding='mbcs').read()
>>> text = open(filename, 'r').read()
>>> # Fix 3: Explicitly decode the path
>>> filename = open('filename_in_mbcs.txt', 'rb').read()
>>> text = open(filename.decode('mbcs'), 'r').read()
在 filename
的创建者与 filename
的使用者分离的情况下,编码是需要包含的重要信息
>>> some_object.filename = r'C:\Users\Steve\Documents\my_file.txt'.encode('mbcs')
>>> filename = some_object.filename
>>> type(filename)
<class 'bytes'>
>>> text = open(filename, 'r').read()
为了修复此代码以获得跨操作系统和 Python 版本的最佳兼容性,应将文件名公开为 str
>>> # Fix 1: Expose as str
>>> some_object.filename = r'C:\Users\Steve\Documents\my_file.txt'
>>> filename = some_object.filename
>>> type(filename)
<class 'str'>
>>> text = open(filename, 'r').read()
或者,需要将用于路径的编码提供给用户。os.fsencode()
(或 sys.getfilesystemencoding()
)是一个可接受的选择,或者可以添加一个包含确切编码的新属性
>>> # Fix 2: Use fsencode
>>> some_object.filename = os.fsencode(r'C:\Users\Steve\Documents\my_file.txt')
>>> filename = some_object.filename
>>> type(filename)
<class 'bytes'>
>>> text = open(filename, 'r').read()
>>> # Fix 3: Expose as explicit encoding
>>> some_object.filename = r'C:\Users\Steve\Documents\my_file.txt'.encode('cp437')
>>> some_object.filename_encoding = 'cp437'
>>> filename = some_object.filename
>>> type(filename)
<class 'bytes'>
>>> filename = filename.decode(some_object.filename_encoding)
>>> type(filename)
<class 'str'>
>>> text = open(filename, 'r').read()
显式使用“mbcs”
在传递给文件系统 API 之前使用“mbcs”显式编码文本的代码现在传递了不正确编码的字节。请注意,在此示例中,filename
的来源不相关,前提是它是一个 str
>>> filename = open('files.txt', 'r').readline().rstrip()
>>> text = open(filename.encode('mbcs'), 'r')
要更正此代码,应在不显式编码的情况下传递字符串,或应使用 os.fsencode()
>>> # Fix 1: Do not encode the string
>>> filename = open('files.txt', 'r').readline().rstrip()
>>> text = open(filename, 'r')
>>> # Fix 2: Use correct encoding
>>> filename = open('files.txt', 'r').readline().rstrip()
>>> text = open(os.fsencode(filename), 'r')
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0529.rst