使用 Python 的 datetime 时间戳,精度为微秒级

datetime timestamp using Python with microsecond level accuracy

我正在尝试在 Windows OS 和 Python 3.10+ 中的 macOS 上获得精确到微秒的时间戳。

在 Windows OS 上,我注意到 Python 的内置 time.time()(与 datetime.fromtimestamp() 配对)和 datetime.datetime.now()似乎有一个较慢的时钟。它们没有足够的分辨率来区分微秒级事件。好消息是 time 函数,如 time.perf_counter()time.time_ns() do 似乎使用了足够快的时钟来测量微秒级事件。

遗憾的是,我不知道如何将它们放入 datetime 对象中。如何将 time.perf_counter()PEP 564 的纳秒分辨率时间函数的输出转换为 datetime 对象?

注意:我不需要纳秒级别的东西,所以丢掉低于 1-μs 的精度也没关系。


当前解

这是我目前的(hacky)解决方案,实际上工作正常,但我想知道是否有更简洁的方法:

import time
from datetime import datetime, timedelta
from typing import Final

IMPORT_TIMESTAMP: Final[datetime] = datetime.now()
INITIAL_PERF_COUNTER: Final[float] = time.perf_counter()


def get_timestamp() -> datetime:
    """Get a high resolution timestamp with μs-level precision."""
    dt_sec = time.perf_counter() - INITIAL_PERF_COUNTER
    return IMPORT_TIMESTAMP + timedelta(seconds=dt_sec)

这几乎和它得到的一样好,因为 C 模块(如果可用)会覆盖 datetime 的纯 Python 实现中定义的所有 类具有快速 C 实现的模块,并且没有挂钩。
参考:python/cpython@cf86e36

注意:

  1. 精度存在固有 sub-microsecond 误差,等于在 datetime.now() 中获取系统时间与获取性能计数器时间之间花费的时间。
  2. 添加 datetimetimedelta 需要 sub-microsecond 性能成本。

根据您的具体用例,如果多次调用,这可能重要也可能不重要。

稍微改进一下:

INITIAL_TIMESTAMP: Final[float] = time.time()
INITIAL_TIMESTAMP_PERF_COUNTER: Final[float] = time.perf_counter()

def get_timestamp_float() -> float:
    dt_sec = time.perf_counter() - INITIAL_TIMESTAMP_PERF_COUNTER
    return INITIAL_TIMESTAMP + dt_sec

def get_timestamp_now() -> datetime:
    dt_sec = time.perf_counter() - INITIAL_TIMESTAMP_PERF_COUNTER
    return datetime.fromtimestamp(INITIAL_TIMESTAMP + dt_sec)

轶事数字

Windows:

# Intrinsic error
timeit.timeit('datetime.now()', setup='from datetime import datetime')/1000000  # 0.31 μs
timeit.timeit('time.time()', setup='import time')/1000000                       # 0.07 μs

# Performance cost
setup = 'from datetime import datetime, timedelta; import time'
timeit.timeit('datetime.now() + timedelta(1.000001)', setup=setup)/1000000            # 0.79 μs
timeit.timeit('datetime.fromtimestamp(time.time() + 1.000001)', setup=setup)/1000000  # 0.44 μs
# Resolution
min get_timestamp_float() delta: 239 ns

Windows 和 macOS:

Windows macOS
# Intrinsic error
timeit.timeit('datetime.now()', setup='from datetime import datetime')/1000000 0.31 μs 0.61 μs
timeit.timeit('time.time()', setup='import time')/1000000 0.07 μs 0.08 μs
# Performance cost
setup = 'from datetime import datetime, timedelta; import time' - -
timeit.timeit('datetime.now() + timedelta(1.000001)', setup=setup)/1000000 0.79 μs 1.26 μs
timeit.timeit('datetime.fromtimestamp(time.time() + 1.000001)', setup=setup)/1000000 0.44 μs 0.69 μs
# Resolution
min time() delta (benchmark) x ms 716 ns
min get_timestamp_float() delta 239 ns 239 ns

239 ns 是 float 在 Unix 时间量级允许的最小差异,如评论中 Kelly Bundy 所述。

x = time.time()
print((math.nextafter(x, 2*x) - x) * 1e9)  # 238.4185791015625

脚本

解析脚本,基于https://www.python.org/dev/peps/pep-0564/#script

import math
import time
from typing import Final

LOOPS = 10 ** 6

INITIAL_TIMESTAMP: Final[float] = time.time()
INITIAL_TIMESTAMP_PERF_COUNTER: Final[float] = time.perf_counter()

def get_timestamp_float() -> float:
    dt_sec = time.perf_counter() - INITIAL_TIMESTAMP_PERF_COUNTER
    return INITIAL_TIMESTAMP + dt_sec

min_dt = [abs(time.time() - time.time())
          for _ in range(LOOPS)]
min_dt = min(filter(bool, min_dt))
print("min time() delta: %s ns" % math.ceil(min_dt * 1e9))

min_dt = [abs(get_timestamp_float() - get_timestamp_float())
          for _ in range(LOOPS)]
min_dt = min(filter(bool, min_dt))
print("min get_timestamp_float() delta: %s ns" % math.ceil(min_dt * 1e9))