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

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 浮点数类型使用 IEEE 754 标准的二进制 64 位格式。以纳秒 (10-9) 为分辨率,对于大于 224 秒(194 天:以纪元时间戳计算为 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 年:以纪元时间戳计算为 2242-03-16)的值,浮点数时间戳会损失精度。以 100 纳秒 (10-7,Windows 上使用的分辨率) 为分辨率,对于大于 229 秒(17 年:以纪元时间戳计算为 1987-01-05)的值,浮点数时间戳会损失精度。

规范

添加 decimal.Decimal 作为时间戳的新类型。Decimal 支持任意时间戳分辨率,支持算术运算且可比较。即使转换可能会损失精度,也可以将 Decimal 强制转换为浮点数。时钟分辨率也可以存储在 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 参数值可以是浮点数或 Decimal,浮点数仍然是默认值以保持向后兼容性。以下函数支持 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 实现的,比浮点数慢,但有一个新的 C 实现即将包含在 CPython 中。

向后兼容性

默认时间戳类型(浮点数)保持不变,因此对向后兼容性或性能没有影响。仅当显式请求时,才会返回新的时间戳类型 decimal.Decimal。

反对意见:时钟精度

计算机时钟和操作系统都不准确,在实践中无法提供纳秒精度。纳秒是执行几个 CPU 指令所需的时间。即使在实时操作系统上,纳秒级测量在开始由更高级别的应用程序处理时就已经过时了。CPU 中的单个缓存未命中将使精度变得毫无价值。

注意

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

备选方案:时间戳类型

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

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

标准

  • 必须能够对时间戳进行算术运算
  • 时间戳必须可比较
  • 任意分辨率,或至少为纳秒分辨率,且不损失精度
  • 为了向后兼容,应该能够将新的时间戳强制转换为浮点数

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

操作系统使用的最佳分辨率为纳秒。在实践中,大多数时钟精度更接近微秒而不是纳秒。因此,使用纳秒的固定分辨率听起来很合理。

纳秒数 (int)

纳秒分辨率足以满足所有当前 C 函数的需求,因此时间戳可以简单地表示为纳秒数,一个整数,而不是浮点数。

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

128 位浮点数

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

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) 切换到标准时间时重复的小时中,夏令时 (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 已被拒绝,因为它不能强制转换为浮点数且具有固定分辨率。一个新的标准时间戳类型就足够了,Decimal 比 datetime.timedelta 更受欢迎。将 datetime.timedelta 转换为浮点数需要显式调用 datetime.timedelta.total_seconds() 方法。

注意

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

整数元组

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

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

已提出不同的格式

  • A: (分子, 分母)
    • 值 = 分子 / 分母
    • 分辨率 = 1 / 分母
    • 分母 > 0
  • B: (秒, 分子, 分母)
    • 值 = 秒 + 分子 / 分母
    • 分辨率 = 1 / 分母
    • 0 <= 分子 < 分母
    • 分母 > 0
  • C: (整数部分, 小数部分, 底数, 指数)
    • 值 = 整数部分 + 小数部分 / 底数指数
    • 分辨率 = 1 / 底数 指数
    • 0 <= 小数部分 < 底数 指数
    • 底数 > 0
    • 指数 >= 0
  • D: (整数部分, 小数部分, 指数)
    • 值 = 整数部分 + 小数部分 / 10指数
    • 分辨率 = 1 / 10 指数
    • 0 <= 小数部分 < 10 指数
    • 指数 >= 0
  • E: (秒, 纳秒)
    • 值 = 秒 + 纳秒 × 10-9
    • 分辨率 = 10 -9(纳秒)
    • 0 <= 纳秒 < 10 9

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

如果时钟频率是任意的并且不能表示为 10 的幂,则格式 (D) 可能无法存储精确值(可能存在精度损失)。格式 (C) 存在类似的问题,但在这种情况下,可以使用底数=频率和指数=1。

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

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

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

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

注意

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

timespec 结构体

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

支持加法、减法和强制转换为浮点数的 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),但它支持算术运算和强制转换为浮点数。

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

备选方案:API 设计

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

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

此 API 被拒绝,因为有必要隐式导入模块来实例化对象(例如,导入 datetime 来创建 datetime.datetime)。导入模块可能会引发异常,并且速度可能很慢,这种行为是意料之外且令人惊讶的。

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

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

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

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

而不是硬编码时间戳的创建方式,可以添加一个新的协议来从分数创建时间戳。

例如,time.time(timestamp=type) 将调用类方法 type.__fromfraction__(numerator, denominator) 来创建指定类型的 timestamp 对象。如果类型不支持该协议,则使用回退: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”的新模块,该模块具有与 time 模块相同的 API,只是它将返回具有高分辨率的时间戳,例如 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

上次修改时间:2023-09-09 17:39:29 GMT