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

Python 增强提案

PEP 615 – 标准库中对 IANA 时区数据库的支持

作者:
Paul Ganssle <paul at ganssle.io>
讨论列表:
Discourse 帖子
状态:
最终
类型:
标准跟踪
创建:
2020年2月22日
Python 版本:
3.9
历史记录:
2020年2月25日,2020年3月29日
替换:
431

目录

重要

此 PEP 是一个历史文档。最新的规范文档现在可以在 zoneinfo 中找到。

×

请参阅 PEP 1 以了解如何提出更改。

摘要

这建议添加一个模块 zoneinfo,以提供支持 IANA 时区数据库的具体时区实现。默认情况下,zoneinfo 将使用系统的时区数据(如果可用);如果系统没有时区数据,库将回退到使用第一方包 tzdata(部署在 PyPI 上)。[d]

动机

datetime 库使用灵活的机制来处理时区:所有转换和时区信息查询都委托给抽象 datetime.tzinfo 基类的子类的实例。[10] 这允许用户实现任意复杂的时区规则,但在实践中,大多数用户只希望支持三种类型的时区:[a]

  1. UTC 及其固定偏移量
  2. 系统本地时区
  3. IANA 时区

在 Python 3.2 中,引入了 datetime.timezone 类来支持第一类时区(使用特殊的 datetime.timezone.utc 单例表示 UTC)。

虽然仍然没有“本地”时区,但在 Python 3.0 中,朴素时区的语义发生了变化,以支持许多“本地时间”操作,并且现在可以从本地时间获取固定时区偏移量。

>>> print(datetime(2020, 2, 22, 12, 0).astimezone())
2020-02-22 12:00:00-05:00
>>> print(datetime(2020, 2, 22, 12, 0).astimezone()
...       .strftime("%Y-%m-%d %H:%M:%S %Z"))
2020-02-22 12:00:00 EST
>>> print(datetime(2020, 2, 22, 12, 0).astimezone(timezone.utc))
2020-02-22 17:00:00+00:00

但是,仍然不支持 IANA 时区数据库(也称为“tz”数据库或 Olson 数据库 [6])中描述的时区。时区数据库是公共领域的,并且被广泛分发——它在许多类 Unix 操作系统上默认存在。数据库的稳定性得到了高度重视:IETF RFC 既用于维护程序(RFC 6557)也用于编译后的二进制(TZif)格式(RFC 8536)。因此,即使标准库发布的频率相对较低,添加对 IANA 数据库编译输出的支持也可能会为最终用户带来巨大的价值。

提案

此 PEP 有三个主要关注点

  1. zoneinfo.ZoneInfo 类的语义(zoneinfo-class
  2. 使用的时区数据源(data-sources
  3. 时区搜索路径配置选项(search-path-config

由于提案的复杂性,设计决策和基本原理不是分别放在“规范”和“基本原理”部分,而是按主题分组。

zoneinfo.ZoneInfo

构造函数

zoneinfo.ZoneInfo 类的初始设计有几个构造函数。

ZoneInfo(key: str)

主构造函数采用单个参数 key,它是一个字符串,指示系统时区数据库中区域文件的名称(例如 "America/New_York""Europe/London"),并返回一个从搜索路径上第一个匹配的数据源构造的 ZoneInfo(有关详细信息,请参阅 data-sources 部分)。所有时区信息都必须在构造时从数据源(通常是 TZif 文件)中急切读取,并且在对象的生命周期内不能更改(此限制适用于所有 ZoneInfo 构造函数)。

如果在搜索路径上找不到匹配的文件(无论是由于系统没有提供时区数据还是因为密钥无效),构造函数将引发 zoneinfo.ZoneInfoNotFoundError,它将是 KeyError 的子类。

此构造函数做出的一个有点不寻常的保证是,使用相同参数的调用必须返回*相同*的对象。具体来说,对于所有 key 的值,以下断言必须始终有效 [b]

a = ZoneInfo(key)
b = ZoneInfo(key)
assert a is b

这样做的原因是,datetime 操作(例如比较、算术)的语义取决于所涉及的日期时间是否表示相同或不同的时区;只有当 dt1.tzinfo is dt2.tzinfo 时,两个日期时间才在同一个时区。[1] 除了避免不必要地大量创建 ZoneInfo 对象带来的少量性能优势之外,提供此保证还可以最大程度地减少最终用户遇到的意外行为。

dateutil.tz.gettz 自 2.7.0 版(2018 年 3 月发布)以来提供了类似的保证。[16]

注意

实现可以决定如何实现缓存行为,但此处做出的保证仅要求只要存在两个对相同构造函数调用的结果的引用,它们就必须是对同一对象的引用。这与引用计数缓存一致,其中当不存在对 ZoneInfo 对象的引用时,这些对象会被弹出(例如,使用 weakref.WeakValueDictionary 实现的缓存)——允许但不强制或建议使用“强”缓存来实现这一点,其中所有 ZoneInfo 对象都会无限期地保持活动状态。

ZoneInfo.no_cache(key: str)

这是一个绕过构造函数缓存的备用构造函数。它与主构造函数相同,但在每次调用时都返回一个新对象。这可能最适合测试目的,或者有意在具有相同名义时区的日期时间之间引入“不同时区”的语义。

即使通过此方法构造的对象本来应该是一个缓存未命中,它也不得被放入缓存中;换句话说,以下断言应该始终为真

>>> a = ZoneInfo.no_cache(key)
>>> b = ZoneInfo(key)
>>> a is not b
ZoneInfo.from_file(fobj: IO[bytes], /, key: str = None)

这是一个备用构造函数,允许从任何 TZif 字节流构造 ZoneInfo 对象。此构造函数采用一个可选参数 key,用于为 __str____repr__ 设置时区的名称(请参阅 Representations)。

与主构造函数不同,它始终构造一个新对象。它偏离主构造函数的缓存行为有两个原因:流对象具有可变状态,因此确定两个输入是否相同很困难或不可能,并且用户从文件构造很可能希望从该文件加载而不是从缓存加载。

ZoneInfo.no_cache 一样,通过此方法构造的对象不得添加到缓存中。

数据更新期间的行为

重要的是,给定 ZoneInfo 对象的行为在其生命周期内不应改变,因为 datetimeutcoffset() 方法同时用于其相等性和散列计算,如果结果在 datetime 的生命周期内发生变化,则可能会破坏所有可散列对象 [3] [4] 的不变式,如果 x == y,则 hash(x) == hash(y) 也必须为真 [c]

考虑到 datetime 不变式的保留以及主构造函数始终在使用相同参数调用时返回相同对象的约定,如果在解释器的运行期间更新了时区数据源,则它不得使任何缓存失效或修改任何现有的 ZoneInfo 对象。但是,新构造的 ZoneInfo 对象应该来自更新的数据源。

这意味着数据源在 ZoneInfo 构造函数的新调用中更新的点主要取决于缓存行为的语义。获得来自更新数据源的 ZoneInfo 对象的唯一保证方法是诱发缓存未命中,可以通过绕过缓存并使用 ZoneInfo.no_cache 或清除缓存来实现。

注意

指定的缓存行为不需要缓存被延迟填充 - 它与规范一致(尽管不建议),可以热切地预填充从未构建过的时区的缓存。

故意使缓存失效

除了 ZoneInfo.no_cache(允许用户绕过缓存)之外,ZoneInfo 还公开了一个 clear_cache 方法来故意使整个缓存或缓存的选择部分失效。

ZoneInfo.clear_cache(*, only_keys: Iterable[str]=None) -> None

如果未传递任何参数,则所有缓存都将失效,并且在缓存清除后,每个键对主 ZoneInfo 构造函数的第一次调用将返回一个新实例。

>>> NYC0 = ZoneInfo("America/New_York")
>>> NYC0 is ZoneInfo("America/New_York")
True
>>> ZoneInfo.clear_cache()
>>> NYC1 = ZoneInfo("America/New_York")
>>> NYC0 is NYC1
False
>>> NYC1 is ZoneInfo("America/New_York")
True

一个可选参数 only_keys 接受一个要从缓存中清除的键的可迭代对象,否则保持缓存不变。

>>> NYC0 = ZoneInfo("America/New_York")
>>> LA0 = ZoneInfo("America/Los_Angeles")
>>> ZoneInfo.clear_cache(only_keys=["America/New_York"])
>>> NYC1 = ZoneInfo("America/New_York")
>>> LA0 = ZoneInfo("America/Los_Angeles")
>>> NYC0 is NYC1
False
>>> LA0 is LA1
True

缓存行为的操作预计是一个利基用例;此功能主要用于促进测试,并允许具有特殊需求的用户根据自己的需要调整缓存失效行为。

字符串表示

ZoneInfo 类的 __str__ 表示将从 key 参数中获取。部分原因是 key 代表字符串的人类可读“名称”,但也因为它是一个用户希望公开的有用参数。需要提供一种机制来公开用于语言之间序列化的键,因为它也是 CLDR(Unicode 通用语言环境数据存储库 [5])等本地化项目的 ​​主键。

一个例子

>>> zone = ZoneInfo("Pacific/Kwajalein")
>>> str(zone)
'Pacific/Kwajalein'

>>> dt = datetime(2020, 4, 1, 3, 15, tzinfo=zone)
>>> f"{dt.isoformat()} [{dt.tzinfo}]"
'2020-04-01T03:15:00+12:00 [Pacific/Kwajalein]'

当未指定 key 时,str 操作不应该失败,而应该返回对象的 __repr__

>>> zone = ZoneInfo.from_file(f)
>>> str(zone)
'ZoneInfo.from_file(<_io.BytesIO object at ...>)'

ZoneInfo__repr__ 是实现定义的,并且在不同版本之间不一定稳定,但它一定不能是有效的 ZoneInfo 键,以避免在具有有效 __str__ 的键派生 ZoneInfo 和已落入 __repr__ 的文件派生 ZoneInfo 之间产生混淆。

由于使用 str() 访问键没有提供检查键是否存在的简单方法(唯一的方法是尝试从中构造一个 ZoneInfo 并检测它是否引发异常),ZoneInfo 对象还将公开一个只读 key 属性,如果未提供键,则该属性将为 None

Pickle 序列化

与其序列化所有转换数据,不如通过键序列化 ZoneInfo 对象,并且从原始文件(即使是为 key 指定了值的那些文件)构造的 ZoneInfo 对象不能被 pickle。

ZoneInfo 对象的行为取决于其构造方式。

  1. ZoneInfo(key):当使用主构造函数构造时,ZoneInfo 对象将按键序列化,并且在反序列化时,将在反序列化过程中使用主构造函数,因此预期它与对同一时区的其他引用的对象相同。例如,如果 europe_berlin_pkl 是一个包含从 ZoneInfo("Europe/Berlin") 构造的 pickle 的字符串,则预期以下行为
    >>> a = ZoneInfo("Europe/Berlin")
    >>> b = pickle.loads(europe_berlin_pkl)
    >>> a is b
    True
    
  2. ZoneInfo.no_cache(key):当从绕过缓存的构造函数构造时,ZoneInfo 对象仍将按键序列化,但在反序列化时,它将使用绕过缓存的构造函数。如果 europe_berlin_pkl_nc 是一个包含从 ZoneInfo.no_cache("Europe/Berlin") 构造的 pickle 的字符串,则预期以下行为
    >>> a = ZoneInfo("Europe/Berlin")
    >>> b = pickle.loads(europe_berlin_pkl_nc)
    >>> a is b
    False
    
  3. ZoneInfo.from_file(fobj, /, key=None):当从文件构造时,ZoneInfo 对象将在 pickling 时引发异常。如果最终用户希望 pickle 从文件构造的 ZoneInfo,建议他们使用包装器类型或自定义序列化函数:通过键序列化或存储文件对象的内容并序列化该内容。

这种序列化方法要求在序列化和反序列化端都提供所需键的时区数据,类似于在序列化和反序列化环境中都预期类和函数的引用存在的方式。这也意味着,当解 pickle 在具有不同版本时区数据的环境中 pickle 的 ZoneInfo 时,不保证结果的一致性。

时区数据来源

IANA 时区支持最困难的挑战之一是保持数据的最新;在 1997 年至 2020 年期间,每年发布 3 到 21 个版本,通常是为了应对时区规则的变化,几乎没有或根本没有通知(有关更多详细信息,请参阅 [7])。为了保持最新,并让系统管理员控制数据源,我们建议尽可能使用系统部署的时区数据。但是,并非所有系统都提供公开可访问的时区数据库 - 值得注意的是,Windows 使用不同的系统来管理时区 - 因此,如果可用,zoneinfo 将回退到可安装的第一方包 tzdata,该包在 PyPI 上可用。 [d] 如果未找到任何系统 zoneinfo 文件但已安装 tzdata,则主 ZoneInfo 构造函数将使用 tzdata 作为时区源。

系统时区信息

许多类 Unix 系统默认部署时区数据,或提供规范的时区数据包(通常称为 tzdata,例如在 Arch Linux、Fedora 和 Debian 上)。只要有可能,最好推迟到系统时区信息,因为这允许所有语言栈的时区信息在一个地方更新和维护。鼓励 Python 发行版尽可能确保时区数据与 Python 一起安装(例如,通过将 tzdata 声明为 python 包的依赖项)。

zoneinfo 模块将使用类似于 PATH 环境变量或 Python 中的 sys.path 变量的“搜索路径”策略;zoneinfo.TZPATH 变量将是只读的(有关更多详细信息,请参阅 search-path-config),按顺序排列要搜索的时区数据位置列表。当从键创建 ZoneInfo 实例时,区域文件将从路径上存在的第一个数据源构造,因此例如,如果 TZPATH

TZPATH = (
    "/usr/share/zoneinfo",
    "/etc/zoneinfo"
    )

并且(尽管这非常不寻常)/usr/share/zoneinfo 只包含 America/New_York 并且 /etc/zoneinfo 包含 America/New_YorkEurope/Moscow,则 ZoneInfo("America/New_York") 将由 /usr/share/zoneinfo/America/New_York 满足,而 ZoneInfo("Europe/Moscow") 将由 /etc/zoneinfo/Europe/Moscow 满足。

目前,在 Windows 系统上,搜索路径将默认为空,因为 Windows 没有正式提供时区数据库的副本。在非 Windows 系统上,搜索路径将默认为最常观察到的搜索路径列表。尽管这可能会在将来的版本中发生变化,但在启动时,默认搜索路径将为

TZPATH = (
    "/usr/share/zoneinfo",
    "/usr/lib/zoneinfo",
    "/usr/share/lib/zoneinfo",
    "/etc/zoneinfo",
)

这可以在编译时或运行时配置;有关配置选项的更多信息,请参阅 search-path-config

tzdata Python 包

为了确保所有最终用户都能轻松访问时区数据,本 PEP 建议创建一个仅包含数据的包 tzdata 作为系统数据不可用时的后备。 tzdata 包将作为 PyPI 上的“第一方”包 [d] 分发,由 CPython 开发团队维护。

tzdata 包仅包含数据和元数据,没有面向公众的函数或类。它将被设计为与更新的 importlib.resources [11] 访问模式和旧的访问模式(如 pkgutil.get_data [12])兼容。

虽然它是专门为 CPython 使用而设计的,但 tzdata 包旨在成为一个独立的公共包,它可以用作第三方 Python 包的时区数据的“官方”来源。

搜索路径配置

时区搜索路径非常依赖于系统,有时甚至依赖于应用程序,因此提供自定义它的选项是有意义的。本 PEP 提供了三种此类自定义途径

  1. 通过编译时选项进行全局配置
  2. 通过环境变量进行每次运行的配置
  3. 通过 reset_tzpath 函数更改运行时配置

在所有配置方法中,搜索路径必须仅包含绝对路径,而不是相对路径。如果发现非绝对路径的字符串,实现可以选择忽略、发出警告或引发异常(并且可以根据上下文做出不同的选择——例如,当将无效路径传递给reset_tzpath时引发异常,而在环境变量中包含无效路径时发出警告)。如果未引发异常,则任何非绝对路径的字符串都不得包含在时区搜索路径中。

编译时选项

下游分发者很可能确切地知道其系统时区数据部署在何处,因此将提供一个编译时选项PYTHONTZPATH来设置默认搜索路径。

PYTHONTZPATH选项应是一个由os.pathsep分隔的字符串,列出时区数据可能部署的位置(例如/usr/share/zoneinfo)。

环境变量

初始化TZPATH时(以及每当reset_tzpath被无参数调用时),zoneinfo模块将使用环境变量PYTHONTZPATH(如果存在)来设置搜索路径。

PYTHONTZPATH是一个由os.pathsep分隔的字符串,它 *替换*(而不是增强)默认时区路径。一些关于提议语义的示例

$ python print_tzpath.py
("/usr/share/zoneinfo",
 "/usr/lib/zoneinfo",
 "/usr/share/lib/zoneinfo",
 "/etc/zoneinfo")

$ PYTHONTZPATH="/etc/zoneinfo:/usr/share/zoneinfo" python print_tzpath.py
("/etc/zoneinfo",
 "/usr/share/zoneinfo")

$ PYTHONTZPATH="" python print_tzpath.py
()

这没有提供用于向默认搜索路径前置或追加的内置机制,因为这些用例可能比较小众。应该可以相当容易地用默认搜索路径填充环境变量。

$ export DEFAULT_TZPATH=$(python -c \
    "import os, zoneinfo; print(os.pathsep.join(zoneinfo.TZPATH))")

reset_tzpath 函数

zoneinfo提供了一个reset_tzpath函数,允许在运行时更改搜索路径。

def reset_tzpath(
    to: Optional[Sequence[Union[str, os.PathLike]]] = None
) -> None:
    ...

当使用路径序列调用此函数时,此函数将zoneinfo.TZPATH设置为由所需值构造的元组。当不带参数或None调用时,此函数将zoneinfo.TZPATH重置为默认配置。

这可能主要用于(永久或临时)禁用系统时区路径的使用,并强制模块使用tzdata包。reset_tzpath不太可能成为常见操作,除非是在对时区配置敏感的测试函数中,但提供一种官方机制来更改此设置似乎比允许大量围绕TZPATH不变性的hack更好。

注意

尽管在运行期间更改TZPATH是被支持的操作,但应建议用户这样做可能会偶尔导致异常语义,并且在进行设计权衡时,将给予使用静态TZPATH更大的权重,因为这是更常见的用例。

构造函数中所述,主要的ZoneInfo构造函数使用缓存来确保两个构造相同的ZoneInfo对象始终比较为相同(即ZoneInfo(key) is ZoneInfo(key)),并且此缓存的性质是实现定义的。这意味着在某些情况下,当在TZPATH的不同值下使用相同的key时,ZoneInfo构造函数的行为可能不可预测地不一致。例如

>>> reset_tzpath(to=["/my/custom/tzdb"])
>>> a = ZoneInfo("My/Custom/Zone")
>>> reset_tzpath()
>>> b = ZoneInfo("My/Custom/Zone")
>>> del a
>>> del b
>>> c = ZoneInfo("My/Custom/Zone")

在此示例中,My/Custom/Zone仅存在于/my/custom/tzdb中,而不是默认搜索路径上。在所有实现中,a的构造函数必须成功。b的构造函数是否成功是实现定义的,但如果成功,则必须为真a is b,因为ab都是对同一键的引用。 c的构造函数是否成功也是实现定义的。zoneinfo的实现 *可以* 返回在之前的构造函数调用中构造的对象,或者它们可能会引发异常失败。

向后兼容性

这不会有任何向后兼容性问题,因为它将创建一个新的API。

只需稍作修改,就可以创建一个支持Python 3.6+的zoneinfo模块的反向移植。

tzdata包旨在“仅包含数据”,并且应该支持它可以构建的任何版本的Python(包括Python 2.7)。

安全隐患

这将需要从磁盘解析zoneinfo数据,主要来自系统位置,但也可能来自用户提供的数据。实现(特别是C代码)中的错误可能导致潜在的安全问题,但相对于解析其他文件类型,没有特别的风险。

由于时区数据键本质上是相对于某个时区根的路径,因此实现应注意避免路径遍历攻击。请求诸如../../../path/to/something之类的键不应泄露有关时区路径之外的文件系统状态的任何信息。

参考实现

初始参考实现可在https://github.com/pganssle/zoneinfo获取

这最终可能会转换为3.6+的反向移植。

被拒绝的想法

构建自定义 tzdb 编译器

使用TZif格式的一个主要问题是,它实际上不包含足够的信息来始终正确确定要为tzinfo.dst()返回的值。这是因为对于任何给定的时区偏移量,TZif仅标记UTC偏移量以及它是否表示DST偏移量,但tzinfo.dst()返回DST偏移的总量,以便可以从datetime.utcoffset() - datetime.dst()重建“标准”偏移量。用于dst()的值可以通过查找等效的STD偏移量并计算差值来确定,但TZif格式未指定哪些偏移量形成STD/DST对,因此必须使用启发式方法来确定这一点。

一个常见的启发式方法——查看最近的标准偏移量——在1992年和1996年葡萄牙的时区变化情况下明显失败,在DST转换期间“标准”偏移量发生了1小时的偏移,导致从STD到DST状态的转换而偏移量没有变化。事实上,有可能(尽管从未发生过)创建一个永久为DST且没有标准偏移量的时区。

虽然此信息在编译后的TZif二进制文件中丢失,但在原始tzdb文件中存在,并且可以自己解析此信息并创建更合适的二进制格式。

这个想法由于以下几个原因而被拒绝

  1. 它排除了使用任何系统部署的时区信息,这些信息通常仅以TZif格式存在。
  2. 原始tzdb格式虽然稳定,但 *不如* TZif格式稳定;一些下游tzdb解析器已经遇到了其自定义解析器的旧部署与最近的tzdb版本不兼容的问题,导致创建了“后卫”格式以简化转换。[8]
  3. 启发式方法目前在dateutilpytz中对于所有已知的时区(历史和现在)都足够了,并且不太可能出现启发式方法无法捕获的新时区——尽管 *当前* 一代启发式方法无法捕获的新规则出现的可能性更大;在这种情况下,需要进行错误修复以适应变化的情况。
  4. dst()方法的实用性(实际上TZif中的isdst参数)从一开始就有些值得怀疑,因为几乎所有有用的信息都包含在utcoffset()tzname()方法中,这些方法不会遇到相同的问题。

简而言之,维护自定义的tzdb编译器或编译包会增加CPython开发团队和系统管理员的维护负担,而其主要好处是解决一个假设性的故障,如果发生,则可能对现实世界的影响很小。

默认情况下在标准库中包含 tzdata

尽管PEP 453(它引入了ensurepip机制到CPython)为在PyPI上维护的标准库模块提供了一个方便的模板,但潜在的类似ensuretzdata机制在某种程度上不太必要,并且会足够复杂,因此被认为超出了此PEP的范围。

由于zoneinfo模块旨在尽可能地使用系统时区数据,因此在部署时区数据的系统上,tzdata包是不必要的(并且可能是不希望的),因此将tzdata与CPython一起发布似乎并不重要。

目前还不清楚如何更新这些混合标准库/PyPI模块(除了pip,它具有用于更新和通知的自然机制),并且由于它对模块的操作并不关键,因此推迟任何此类提议似乎是谨慎的。

对闰秒的支持

除了时区偏移和名称规则之外,IANA时区数据库还提供了闰秒数据源。这被认为超出了范围,因为datetime.datetime目前不支持闰秒,并且闰秒数据问题可以推迟到添加闰秒支持时再处理。

第一方tzdata包应附带闰秒数据,即使zoneinfo模块不使用它。

使用类似 pytz 的接口

PEP 431中曾提出过一个类似于pytz[18])的接口,但最终由于缺乏对模糊日期时间支持而被撤回/拒绝。 PEP 495添加了fold属性来解决这个问题,但fold使得pytz的非标准tzinfo类不再必要,因此不再需要类似pytz的接口。[2]

zoneinfo方法更接近于dateutil.tz,后者在Python 3.6发布前不久实现了对fold的支持(包括向旧版本的移植)。

通过 Microsoft 的 ICU API 支持 Windows

Windows没有将时区数据库作为TZif文件发布,但从Windows 10的2017创意者更新开始,微软提供了一个与国际组件Unicode (ICU)项目[13] [14]交互的API,其中包含一个用于访问时区数据的API——数据来源是IANA时区数据库。[15]

提供此API的绑定将允许我们“开箱即用”地支持Windows,而无需安装tzdata包,但不幸的是,Windows提供的C头文件无法访问底层时区数据——仅提供了一个查询系统以获取转换和偏移信息的API。这将限制任何基于ICU的实现的语义,这些语义可能与非基于ICU的实现不兼容——尤其是在缓存的行为方面。

由于ICU似乎不能简单地用作ZoneInfo对象的附加数据源,因此本PEP认为ICU支持超出范围,可能更适合由第三方库支持。

替代的环境变量配置

本PEP建议使用单个环境变量:PYTHONTZPATH。这是基于这样的假设:大多数想要操作时区路径的用户都希望完全替换它(例如,“我确切地知道我的时区数据在哪里”),而其他用例(例如,在现有搜索路径前添加)则不太常见。

我们考虑并拒绝了其他几种方案。

  1. PYTHON_TZPATH分成两个环境变量:DEFAULT_PYTHONTZPATHPYTHONTZPATH,其中PYTHONTZPATH包含要附加(或前置)到默认时区路径的值,而DEFAULT_PYTHONTZPATH将*替换*默认时区路径。我们拒绝了这个方案,因为它可能会导致用户混淆,如果主要用例是替换而不是增强。
  2. 添加PYTHONTZPATH_PREPENDPYTHONTZPATH_APPEND或两者,以便用户可以在任一端增强搜索路径,而无需尝试确定默认时区路径是什么。我们拒绝了这个方案,因为它可能是多余的,并且因为如果对该功能有很大需求,可以在将来的更新中以向后兼容的方式轻松添加。
  3. 仅使用PYTHONTZPATH变量,但提供一个表示默认时区路径的自定义特殊值,例如<<DEFAULT_TZPATH>>,以便用户可以附加到时区路径,例如PYTHONTZPATH=<<DEFAULT_TZPATH>>:/my/path可用于将/my/path附加到时区路径的末尾。

    此方案的一个优点是,它为在搜索路径上指定非基于文件元素添加了一个自然的扩展点,例如,如果tzdata存在,则更改其优先级,或者如果将来向库中添加对TZDIST的原生支持。

    我们拒绝了这个方案,主要是因为这些类型的特殊值通常不会出现在PATH类型的变量中,并且目前唯一提议的用例是默认TZPATH的替代品,可以通过执行Python程序来查询默认值获取它。拒绝此方案的另一个因素是,由于PYTHONTZPATH仅接受绝对路径,因此任何不表示有效绝对路径的字符串都隐式地保留供将来使用,因此可以在库的未来版本中以向后兼容的方式根据需要引入这些特殊值。

使用 datetime 模块

一个可能的思路是将ZoneInfo添加到datetime模块中,而不是为它提供一个单独的模块。本PEP更倾向于使用单独的zoneinfo模块,尽管嵌套的datetime.zoneinfo模块也在考虑之中。

反对将 ZoneInfo 直接放入 datetime 中的论点

datetime模块已经有点拥挤了,因为它有很多行为比较复杂的类——datetime.datetimedatetime.datedatetime.timedatetime.timedeltadatetime.timezonedatetime.tzinfo。该模块的实现和文档已经非常复杂,如果可以避免的话,最好不要使问题更加复杂。

ZoneInfo类在某些方面也与datetime提供的其他所有类有所不同;其他类都旨在成为精简、简单的类型,而ZoneInfo类则更复杂:它是一个特定格式(TZif)的解析器,是存储在该格式中的信息的表示,以及一种在系统中已知位置查找信息的方法。

最后,虽然确实有人需要zoneinfo模块也需要datetime模块,但反过来不一定成立:许多人希望在没有zoneinfo的情况下使用datetime。考虑到zoneinfo可能会引入其他可能更重量级的标准库模块,最好允许这两个模块分别导入——特别是如果Python未来有潜在的“tree shaking”发行版。[9]

最终分析,将zoneinfo保留为具有单独文档页面的独立模块,而不是将其类和函数直接放入datetime中,这是合理的。

使用 datetime.zoneinfo 而不是 zoneinfo

一个更易于接受的配置可能是将zoneinfo作为模块嵌套在datetime下,如datetime.zoneinfo

支持此方案的理由

  1. 它将zoneinfodatetime一起整齐地命名空间化。
  2. timezone类已经在datetime中了,某些时区在datetime中,而其他时区在顶级模块中,这可能看起来很奇怪。
  3. 如前所述,导入zoneinfo必然需要导入datetime,因此要求导入父模块并不算强加。

反对此方案的理由

  1. 为了避免强制所有datetime用户导入zoneinfozoneinfo模块需要延迟导入,这意味着最终用户需要显式导入datetime.zoneinfo(而不是导入datetime并在模块上访问zoneinfo属性)。这是dateutil的工作方式(所有子模块都是延迟导入),并且一直是最终用户混淆的根源。

    可以使用模块级的__getattr____dir__(根据PEP 562)避免最终用户的这种令人困惑的要求,但这会增加datetime模块实现的一些复杂性。模块或类中的这种行为往往会使静态分析工具感到困惑,对于像datetime这样广泛使用且至关重要的库来说,这可能不是我们想要的。

  2. 将实现嵌套在datetime下可能需要将datetime从单个文件模块(datetime.py)重构为具有__init__.py的目录。这是一个次要问题,但datetime模块的结构已经稳定了很多年,如果可能的话,最好避免这种变化。

    这个顾虑*可以*通过将zoneinfo实现为_zoneinfo.py并在datetime内部将其导入为zoneinfo来缓解,但这在美观或代码组织方面似乎不可取,并且它将排除最终用户需要显式导入datetime.zoneinfo的嵌套版本。

本PEP认为,总的来说,最好使用单独的顶级zoneinfo模块,因为嵌套的好处并不大到足以压倒实际的实现问题。

脚注

[a]
声称绝大多数用户只需要几种时区类型是基于轶事印象,而不是任何科学依据。举个例子,dateutil提供了许多时区类型,但用户支持主要集中在这三种类型上。
[b]
如果用户故意清除时区缓存,则构造相同的ZoneInfo对象应该为相同对象的陈述可能会被违反。
[c]
给定datetime的哈希值在第一次计算时被缓存,因此我们不必担心一个给定的datetime对象的哈希值在其生命周期内可能会发生变化的更严重问题。
[d] (1, 2, 3)
这里的“第一方”与“第三方”的区别在于,尽管它是通过PyPI分发的,并且目前尚未默认包含在Python中,但它应被视为CPython的官方子项目,而不是一个“受认可的”第三方包。

参考文献

其他时区实现


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

上次修改时间: 2024-06-01 20:10:03 GMT