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()
)。
当路径在文件系统和应用程序之间传递时,它们要么作为字节 blob 传递,要么使用os.fsencode()
和os.fsdecode()
转换为/从 str 转换,或者使用sys.getfilesystemencoding()
显式编码。使用sys.getfilesystemencoding()
编码字符串的结果是在默认文件系统的本机格式中的字节 blob。
在 Windows 上,文件系统的本机格式为 utf-16-le。访问文件系统的推荐平台 API 都接受并返回以这种格式编码的文本。但是,在 Windows NT 之前(以及可能更早),本机格式是可配置的机器选项,并且存在一组单独的 API 来接受此格式。该选项(“活动代码页”)和这些 API(“*A 函数”)在 Windows 的最新版本中仍然存在,以实现向后兼容性,尽管新功能通常仅具有 utf-16-le API(“*W 函数”)。
在 Python 中,推荐使用 str,因为它可以正确地往返路径中使用的所有字符(在 POSIX 上使用代理转义处理;在 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()
或任何将返回类型与参数类型匹配的函数中都可以观察到相同的结果。
虽然一个用户可访问的解决方案是在任何地方都使用 str,但 POSIX 系统在仅使用字节时通常不会遭受数据丢失,因为字节是规范表示。即使编码根据某些标准“不正确”,文件系统仍会将字节映射回文件。利用这一点可以避免解码和重新编码的成本,这样(理论上,并且仅在 POSIX 上),如下代码可能由于使用b'.'
而不是使用'.'
而更快。
>>> for f in os.listdir(b'.'):
... os.stat(f)
...
因此,专注于 POSIX 的库作者更喜欢使用字节来表示路径。对于某些作者来说,这也很方便,因为他们的代码可能已经接收了已知编码正确的字节,而其他作者则试图简化其代码从 Python 2 的移植。但是,正确性假设不适用于 Windows,在 Windows 中 Unicode 是规范表示,并且可能会导致错误。这种潜在的数据丢失是为什么在 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 字节)是为了确保能够往返路径名并允许基本操作(例如,使用os.path
模块),同时假设 ASCII 兼容编码。使用 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
上次修改时间:2023-09-09 17:39:29 GMT