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() 等。复制每个模块的代码是不可接受的。
链接
Python
- 问题 #7652:将 decimal 的 C 版本合并到 py3k 中 (cdecimal)
- 问题 #11457:os.stat():添加新字段以获取作为具有纳秒分辨率的 Decimal 对象的时间戳
- 问题 #13882:PEP 410:使用 decimal.Decimal 类型作为时间戳
- [Python-Dev] 将时间戳存储为 decimal.Decimal 对象
其他语言
- Ruby (1.9.3),Time 类 支持皮秒 (10-12)
- .NET 框架,DateTime 类型:自 0001 年 1 月 1 日午夜 12:00:00 以来经过的 100 纳秒间隔数。DateTime.Ticks 使用带符号的 64 位整数。
- Java (1.5),System.nanoTime():具有未指定起点的挂钟,以纳秒数表示,使用带符号的 64 位整数 (long)。
- Perl,Time::Hiref 模块:使用浮点数,因此与 Python 浮点时间戳一样,在纳秒分辨率方面存在相同的精度损失问题
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0410.rst
上次修改时间:2023-09-09 17:39:29 GMT