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

Python 增强提案

PEP 410 – 对时间戳使用 decimal.Decimal 类型

作者:
Victor Stinner <vstinner at python.org>
状态:
已拒绝
类型:
标准跟踪
创建日期:
2012 年 2 月 1 日
Python 版本:
3.3
决议:
Python-Dev 消息

目录

拒绝通知

此 PEP 被拒绝。请参阅 https://mail.python.org/pipermail/python-dev/2012-February/116837.html

摘要

Decimal 成为高精度时间戳的官方类型,以便 Python 支持使用纳秒分辨率且无精度损失的新功能。

基本原理

Python 2.3 引入了浮点数时间戳以支持亚秒级分辨率。自 Python 2.5 起,os.stat() 默认使用浮点数时间戳。Python 3.3 引入了支持纳秒分辨率的函数

  • os 模块:futimens(), utimensat()
  • time 模块:clock_gettime(), clock_getres(), monotonic(), wallclock()

os.stat() 读取纳秒时间戳,但返回浮点数时间戳。

Python 的 float 类型使用 IEEE 754 标准的 binary64 格式。当分辨率为一纳秒 (10-9) 时,对于大于 224 秒(194 天:Epoch 时间戳为 1970-07-14)的值,浮点数时间戳会丢失精度。

设置支持纳秒时间戳的文件系统(例如 ext4、btrfs、NTFS 等)的精确修改时间需要纳秒分辨率。它也有助于比较修改时间以检查一个文件是否比另一个文件新。用例:使用 shutil.copystat() 复制文件的修改时间,使用 tarfile 模块创建 TAR 存档,使用 mailbox 模块管理邮箱等。

与固定分辨率(如纳秒)相比,任意分辨率更受欢迎,这样在需要更高分辨率时就不必更改 API。例如,NTP 协议使用 232 秒的几分之一(约 2.3 × 10-10 秒),而 NTP 协议版本 4 使用 264 秒的几分之一(5.4 × 10-20 秒)。

注意

当分辨率为 1 微秒 (10-6) 时,对于大于 233 秒(272 年:Epoch 时间戳为 2242-03-16)的值,浮点数时间戳会丢失精度。当分辨率为 100 纳秒 (10-7,Windows 使用的分辨率) 时,对于大于 229 秒(17 年:Epoch 时间戳为 1987-01-05)的值,浮点数时间戳会丢失精度。

规范

将 decimal.Decimal 添加为时间戳的新类型。Decimal 支持任何时间戳分辨率,支持算术运算且可比较。可以将 Decimal 强制转换为 float,尽管转换可能会丢失精度。时钟分辨率也可以存储在 Decimal 对象中。

向以下函数添加一个可选的 *timestamp* 参数

  • os 模块:fstat(), fstatat(), lstat(), stat() (stat 结构的 st_atime, st_ctime 和 st_mtime 字段), sched_rr_get_interval(), times(), wait3() 和 wait4()
  • resource 模块:getrusage() 的 ru_utime 和 ru_stime 字段
  • signal 模块:getitimer(), setitimer()
  • time 模块:clock(), clock_gettime(), clock_getres(), monotonic(), time() 和 wallclock()

*timestamp* 参数值可以是 float 或 Decimal,为保持向后兼容性,float 仍然是默认值。以下函数支持 Decimal 作为输入

  • datetime 模块:date.fromtimestamp(), datetime.fromtimestamp() 和 datetime.utcfromtimestamp()
  • os 模块:futimes(), futimesat(), lutimes(), utime()
  • select 模块:epoll.poll(), kqueue.control(), select()
  • signal 模块:setitimer(), sigtimedwait()
  • time 模块:ctime(), gmtime(), localtime(), sleep()

os.stat_float_times() 函数已被弃用:请改用 int() 进行显式转换。

注意

decimal 模块是用 Python 实现的,比 float 慢,但有一个新的 C 实现,已接近包含在 CPython 中。

向后兼容性

默认时间戳类型(float)未改变,因此对向后兼容性或性能没有影响。新的时间戳类型 decimal.Decimal 仅在明确请求时返回。

异议:时钟精度

计算机时钟和操作系统不准确,实际上无法提供纳秒精度。执行几条 CPU 指令只需要一纳秒。即使在实时操作系统上,纳秒级精度测量在更高层应用程序开始处理时就已经过时了。CPU 中的一次缓存未命中将使精度毫无价值。

注意

Linux *实际上* 能够以纳秒精度测量时间,尽管它无法以纳秒精度将其时钟与 UTC 同步。

替代方案:时间戳类型

为了支持任意分辨率或纳秒分辨率的时间戳,已考虑以下类型

  • decimal.Decimal
  • 纳秒数
  • 128 位浮点数
  • datetime.datetime
  • datetime.timedelta
  • 整数元组
  • timespec 结构

标准

  • 必须能够对时间戳进行算术运算
  • 时间戳必须可比较
  • 任意分辨率,或至少纳秒分辨率而不会丢失精度
  • 应该能够将新的时间戳强制转换为 float 以保持向后兼容性

一纳秒的分辨率足以支持所有当前 C 函数。

操作系统使用的最佳分辨率是一纳秒。实际上,大多数时钟精度更接近微秒而不是纳秒。因此,使用一纳秒的固定分辨率似乎是合理的。

纳秒数(整数)

纳秒分辨率足以支持所有当前的 C 函数,因此时间戳可以简单地是一个整数(纳秒数),而不是浮点数。

纳秒数格式已被拒绝,因为它需要为该格式添加新的专用函数,因为仅通过检查对象类型无法区分纳秒数和秒数。

128 位浮点数

添加一个新的 IEEE 754-2008 四倍精度二进制浮点数类型。IEEE 754-2008 四倍精度浮点数有 1 位符号,15 位指数和 112 位尾数。128 位浮点数由 GCC (4.3)、Clang 和 ICC 编译器支持。

Python 必须是可移植的,因此不能依赖仅在某些平台上可用的类型。例如,Visual C++ 2008 不支持 128 位浮点数,而它被用于构建官方 Windows 可执行文件。另一个例子:GCC 4.3 在 x86 上的 32 位模式下不支持 __float128(但 GCC 4.4 支持)。

还有一个许可问题:GCC 使用 MPFR 库进行 128 位浮点数运算,该库在 GNU LGPL 许可下分发。此许可与 Python 许可不兼容。

注意

Intel CPU 的 x87 浮点单元支持 80 位浮点数。SSE 指令集不支持此格式,SSE 指令集现在比浮点数更受青睐,尤其是在 x86_64 上。其他 CPU 供应商不支持 80 位浮点数。

datetime.datetime

datetime.datetime 类型是时间戳的自然选择,因为它清楚地表明该类型包含一个时间戳,而 int、float 和 Decimal 只是原始数字。它是一个绝对时间戳,因此定义明确。它可以直接访问年、月、日、小时、分钟和秒。它具有与时间相关的函数,例如将时间戳格式化为字符串的方法(例如 datetime.datetime.strftime)。

主要问题是,除了 os.stat()、time.time() 和 time.clock_gettime(time.CLOCK_GETTIME) 之外,所有时间函数都有一个未指定的起点且没有时区信息,因此无法转换为 datetime.datetime。

datetime.datetime 也有时区问题。例如,没有时区的 datetime 对象(无时区)和有时区的 datetime 对象(有时区)无法进行比较。在从 DST 转换为正常时间时的重复小时也存在排序问题。

datetime.datetime 被拒绝,因为它不能用于起点未指定的函数,如 os.times() 或 time.clock()。

对于 time.time() 和 time.clock_gettime(time.CLOCK_GETTIME):已经可以使用以下方式获取当前时间的 datetime.datetime 对象

datetime.datetime.now(datetime.timezone.utc)

对于 os.stat(),可以轻松地从 UTC 时区的 decimal.Decimal 时间戳创建 datetime.datetime 对象

datetime.datetime.fromtimestamp(value, datetime.timezone.utc)

注意

datetime.datetime 只支持微秒分辨率,但可以增强以支持纳秒。

datetime.timedelta

datetime.timedelta 是相对时间戳的自然选择,因为它清楚地表明该类型包含一个时间戳,而 int、float 和 Decimal 只是原始数字。当起点已知时,它可以与 datetime.datetime 一起使用以获取绝对时间戳。

datetime.timedelta 被拒绝,因为它不能强制转换为 float 并且分辨率固定。一个新标准时间戳类型就足够了,Decimal 比 datetime.timedelta 更受青睐。将 datetime.timedelta 转换为 float 需要显式调用 datetime.timedelta.total_seconds() 方法。

注意

datetime.timedelta 只支持微秒分辨率,但可以增强以支持纳秒。

整数元组

为了在 Python 中公开 C 函数,整数元组是存储时间戳的自然选择,因为 C 语言使用具有整数字段的结构(例如 timeval 和 timespec 结构)。仅使用整数可以避免精度损失(Python 支持任意长度的整数)。创建和解析整数元组既简单又快速。

根据元组的确切格式,精度可以是任意的或固定的。可以选择精度,因为精度损失小于任意限制(如一纳秒)。

已提出不同的格式

  • (分子, 分母)
    • 值 = 分子 / 分母
    • 分辨率 = 1 / 分母
    • 分母 > 0
  • (整数部分, 分子, 分母)
    • 值 = 整数部分 + 分子 / 分母
    • 分辨率 = 1 / 分母
    • 0 <= 分子 < 分母
    • 分母 > 0
  • (整数部分, 浮点部分, 基数, 指数)
    • 值 = 整数部分 + 浮点部分 / 基数指数
    • 分辨率 = 1 / 基数指数
    • 0 <= 浮点部分 < 基数指数
    • 基数 > 0
    • 指数 >= 0
  • (整数部分, 浮点部分, 指数)
    • 值 = 整数部分 + 浮点部分 / 10指数
    • 分辨率 = 1 / 10指数
    • 0 <= 浮点部分 < 10指数
    • 指数 >= 0
  • (秒, 纳秒)
    • 值 = 秒 + 纳秒 × 10-9
    • 分辨率 = 10-9 (纳秒)
    • 0 <= 纳秒 < 109

除格式 (E) 外,所有格式都支持任意分辨率。

如果时钟频率是任意的且不是 10 的幂,格式 (D) 可能无法存储精确值(可能丢失精度)。格式 (C) 也有类似问题,但在此情况下,可以使用 base=frequency 和 exponent=1。

格式 (C)、(D) 和 (E) 允许在基数为 2 时优化转换为 float,在基数为 10 时优化转换为 decimal.Decimal。

格式 (A) 是一个简单的分数。它支持任意精度,简单(只有两个字段),只需一个简单的除法即可获得浮点值,并且已经被 float.as_integer_ratio() 使用。

为了简化实现(特别是 C 实现以避免整数溢出),可以接受分子大于分母。元组稍后可能会被规范化。

整数元组已被拒绝,因为它们不支持算术运算。

注意

在 Windows 上,QueryPerformanceCounter() 时钟使用处理器的频率,这是一个任意数字,因此可能不是 2 或 10 的幂。可以使用 QueryPerformanceFrequency() 读取频率。

timespec 结构

timespec 是用于存储纳秒分辨率时间戳的 C 结构。Python 可以使用具有相同结构的类型:(秒,纳秒)。为方便起见,支持 timespec 上的算术运算。

支持加法、减法和强制转换为 float 的不完整的 timespec 类型的示例

class timespec(tuple):
    def __new__(cls, sec, nsec):
        if not isinstance(sec, int):
            raise TypeError
        if not isinstance(nsec, int):
            raise TypeError
        asec, nsec = divmod(nsec, 10 ** 9)
        sec += asec
        obj = tuple.__new__(cls, (sec, nsec))
        obj.sec = sec
        obj.nsec = nsec
        return obj

    def __float__(self):
        return self.sec + self.nsec * 1e-9

    def total_nanoseconds(self):
        return self.sec * 10 ** 9 + self.nsec

    def __add__(self, other):
        if not isinstance(other, timespec):
            raise TypeError
        ns_sum = self.total_nanoseconds() + other.total_nanoseconds()
        return timespec(*divmod(ns_sum, 10 ** 9))

    def __sub__(self, other):
        if not isinstance(other, timespec):
            raise TypeError
        ns_diff = self.total_nanoseconds() - other.total_nanoseconds()
        return timespec(*divmod(ns_diff, 10 ** 9))

    def __str__(self):
        if self.sec < 0 and self.nsec:
            sec = abs(1 + self.sec)
            nsec = 10**9 - self.nsec
            return '-%i.%09u' % (sec, nsec)
        else:
            return '%i.%09u' % (self.sec, self.nsec)

    def __repr__(self):
        return '<timespec(%s, %s)>' % (self.sec, self.nsec)

timespec 类型类似于整数元组的格式 (E),不同之处在于它支持算术运算和强制转换为 float。

timespec 类型被拒绝,因为它只支持纳秒分辨率并且需要实现每个算术运算,而 Decimal 类型已经实现并且经过了充分的测试。

替代方案:API 设计

添加一个字符串参数来指定返回类型

向返回时间戳的函数添加一个字符串参数,例如:time.time(format="datetime")。字符串比类型更具扩展性:可以请求没有类型的格式,例如整数元组。

此 API 被拒绝,因为它需要隐式导入模块来实例化对象(例如,import datetime 来创建 datetime.datetime)。导入模块可能会引发异常,也可能很慢,这种行为是出乎意料且令人惊讶的。

添加一个全局标志来更改时间戳类型

可以添加一个全局标志,如 os.stat_decimal_times(),类似于 os.stat_float_times(),以全局设置时间戳类型。

全局标志可能会导致期望 float 而不是 Decimal 的库和应用程序出现问题。Decimal 与 float 不完全兼容。例如,float+Decimal 会引发 TypeError。os.stat_float_times() 的情况不同,因为 int 可以强制转换为 float,而 int+float 会得到 float。

添加一个协议来创建时间戳

与其硬编码时间戳的创建方式,不如添加一个新协议来从分数创建时间戳。

例如,time.time(timestamp=type) 将调用类方法 type.__fromfraction__(numerator, denominator) 来创建指定类型的时戳对象。如果类型不支持该协议,则会使用回退:type(numerator) / type(denominator)。

一种变体是使用“转换器”回调来创建时间戳。例如,创建浮点时间戳

def timestamp_to_float(numerator, denominator):
    return float(numerator) / float(denominator)

time、datetime 和其他模块可以提供通用转换器,或者可能是一个特定的“hires”模块。用户可以定义自己的转换器。

这种协议有一个限制:时间戳结构一旦确定就不能更改。例如,添加时区或时间戳的绝对开始点将破坏 API。

该协议提议被认为是过度的,因为它超出了要求,但提出的特定语法(time.time(timestamp=type))允许在发现引人注目的用例时稍后引入它。

注意

也可以使用其他格式而不是分数:例如,请参阅整数元组部分。

向 os.stat 添加新字段

为了以纳秒分辨率获取文件的创建、修改和访问时间,可以在 os.stat() 结构中添加三个字段。

新字段可以是纳秒分辨率的时间戳(例如 Decimal)或每个时间戳的纳秒部分(int)。

如果新字段是纳秒分辨率的时间戳,则填充额外字段将非常耗时。任何对 os.stat() 的调用都会变慢,即使 os.stat() 仅用于检查文件是否存在。可以向 os.stat() 添加一个参数来使这些字段成为可选的,结构将具有可变数量的字段。

如果新字段仅包含小数部分(纳秒),则 os.stat() 将高效。这些字段将始终存在,因此如果操作系统不支持亚秒级分辨率,则设置为零。将时间戳分成两部分(秒和纳秒)类似于 timespec 类型和整数元组,因此具有相同的缺点。

向 os.stat() 结构添加新字段并不能解决其他模块(例如 time 模块)中的纳秒问题。

添加一个布尔参数

因为我们只需要一种新类型(Decimal),可以添加一个简单的布尔标志。例如:time.time(decimal=True) 或 time.time(hires=True)。

这种标志需要进行隐藏导入,这被认为是不好的做法。

布尔参数 API 被拒绝,因为它不“Pythonic”。与布尔参数(标志)相比,更倾向于使用参数值来更改返回类型。

添加新函数

为每种类型添加新函数,例如

  • time.clock_decimal()
  • time.time_decimal()
  • os.stat_decimal()
  • os.stat_timespec()
  • 等等。

为每个创建时间戳的函数添加一个新函数会复制大量代码,并且维护起来会很麻烦。

添加一个名为 hires 的新模块

添加一个名为“hires”的新模块,其 API 与 time 模块相同,但它会返回高分辨率时间戳,例如 decimal.Decimal。添加新模块可以避免将 time 或 os 等底层模块链接到 decimal 模块。

这个想法被拒绝了,因为它需要复制 time 模块的大部分代码,维护起来很麻烦,而且时间戳被 time 模块以外的模块使用。例如:signal.sigtimedwait()、select.select()、resource.getrusage()、os.stat() 等。复制每个模块的代码是不可接受的。


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

最后修改:2025-02-01 08:59:27 GMT