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() 等。复制每个模块的代码是不可接受的。
链接
Python
- Issue #7652: Merge C version of decimal into py3k (cdecimal)
- Issue #11457: os.stat(): add new fields to get timestamps as Decimal objects with nanosecond resolution
- Issue #13882: PEP 410: Use decimal.Decimal type for timestamps
- [Python-Dev] Store timestamps as decimal.Decimal objects
其他语言
- 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::HiRes 模块:使用 float,因此存在与 Python 浮点数时间戳相同的纳秒分辨率精度损失问题
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0410.rst