FEATURED · 精选文章

NumPy Datetime C API 详解:datetime64 的底层数据表示与 ISO 8601 转换函数

发布时间 / 2026/9/20 9:25:44
来源 / 创域科博编辑部
栏目 / 资讯中心
NumPy Datetime C API 详解:datetime64 的底层数据表示与 ISO 8601 转换函数 科学计算数据分析【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址https://gitcode.com/gh_mirrors/nu/numpy点击查看免费下载本篇文章以 Datetime API 官方 C API 参考文档 为主体深入解析 NumPy 如何在 C 层面表示datetime64与timedelta64int64 计数器 单元元数据并逐项剖析NpyDatetime_*系列转换函数的设计意图、参数语义与边界行为。读完本文你将能够理解 datetime64 的底层存储模型、NPY_DATETIMEUNIT单元的精度层级以及如何在 C 扩展代码中安全地在 ISO 8601 字符串、npy_datetimestruct与 Pythondatetime对象之间往返转换。底层设计int64 计数器 单元元数据NumPy 的日期时间datetime在内部用一个npy_int64即int64_t计数器表示配以一份描述时间单元的元数据结构时间差timedelta同样由 int64 与单元元数据组成。这一设计决定了 datetime64 的两个关键性质所有日期时间最终都换算成相对 1970 年 1 月 1 日Unix 纪元的整数计数只是计数的“刻度”随单元不同而变化精度由元数据中的单元base与乘数num共同决定例如M8[10ms]表示每个计数为 10 毫秒。C API 提供的全部NpyDatetime_*函数其共同使命就是在三种表示之间转换ISO 8601 日期字符串如2010-04-16T13:45:18NumPy datetime 的 int64 计数配合单元元数据Python 的datetime.datetime/datetime.date对象。核心数据类型PyArray_DatetimeMetaData单元元数据typedef struct { NPY_DATETIMEUNIT base; int num; } PyArray_DatetimeMetaData;basedatetime 的时间单元见下节的NPY_DATETIMEUNIT枚举num单元的乘数即“每个计数代表 base 的 num 倍”。例如num 10、base NPY_FR_ms时一个计数代表 10 毫秒。该结构体在 numpy/_core/include/numpy/ndarraytypes.h 中定义。值得注意的一点是num乘数可能引发整数溢出——文档在NpyDatetime_ConvertDatetimeStructToDatetime64一节明确指出当num很大时该函数可能发生整数溢出这正是实现中引入溢出检查的原因下文详述。npy_datetimestruct日期时间的“分解视图”typedef struct { npy_int64 year; npy_int32 month, day, hour, min, sec, us, ps, as; } npy_datetimestruct;该结构体把日期时间“炸开”成年、月、日、时、分、秒以及微秒us、皮秒ps、阿秒as共 9 个字段方便逐字段读取或构造。它定义在 numpy/_core/include/numpy/ndarraytypes.h头文件注释补充了一个关键约定NaTNot-a-Time通过year NPY_DATETIME_NAT来表示其中NPY_DATETIME_NAT等于NPY_MIN_INT64即-NPY_MAX_INT64 - 1。这与 int64 计数的表示保持了一致NPY_DATETIME_NAT也被用作 datetime64 计数中的 NaT 哨兵值。NPY_DATETIMEUNIT时间单元枚举NPY_DATETIMEUNIT枚举列出了 NumPy 支持的全部时间单元名称中的 FR 是 frequency频率的缩写。枚举定义于 numpy/_core/include/numpy/ndarraytypes.h从源码可见其完整取值枚举值数值含义NPY_FR_ERROR-1错误或未确定的单元NPY_FR_Y0年NPY_FR_M1月NPY_FR_W2周NPY_FR_B31.6 时代的遗留空位ABI 兼容保留NPY_FR_D4日NPY_FR_h5小时NPY_FR_m6分钟NPY_FR_s7秒NPY_FR_ms8毫秒NPY_FR_us9微秒NPY_FR_ns10纳秒NPY_FR_ps11皮秒NPY_FR_fs12飞秒NPY_FR_as13阿秒NPY_FR_GENERIC14未绑定单元可转换为任意单元注意两个细节NPY_FR_ERROR被刻意定为-1源码注释Force signed enum type, must be -1 for code compatibility因此它常被当作“未知单元”传入各函数NPY_FR_W 2与NPY_FR_D 4之间隔着数值 3 的NPY_FR_B空位——这是为 NumPy 1.6 ABI 兼容保留的缺口头文件注释明确说明因此NPY_DATETIME_NUMUNITS在数值上比实际单元数多 1。从语义上可以把这些单元分成两组非线性单元年Y、月M它们依赖具体的日历闰年、每月天数无法用固定秒数换算线性单元周及以下W/D/h/m/s/ms/us/ns/ps/fs/as可以按固定倍数互相换算。转换函数全景本节的 6 个函数覆盖了日期时间表示的三个方向int64 计数 ↔ 分解结构体、Python 对象 → 分解结构体、ISO 8601 字符串 → 分解结构体、分解结构体 → ISO 8601 字符串。1. NpyDatetime_ConvertDatetimeStructToDatetime64分解结构体 → int64 计数int NpyDatetime_ConvertDatetimeStructToDatetime64( PyArray_DatetimeMetaData *meta, const npy_datetimestruct *dts, npy_datetime *out)将npy_datetimestruct转换为meta指定单元下的 datetime 计数前提是日期已被假定为合法。返回 0 表示成功-1 表示失败。实现位于 numpy/_core/src/multiarray/datetime.c从中可以读出明确的转换算法若dts-year NPY_DATETIME_NAT直接输出 NaTbase NPY_FR_Yret year - 1970年份计数base NPY_FR_Mret 12 * (year - 1970) (month - 1)自 1970-01 起的月数其余单元先通过get_datetimestruct_days(dts)计算距 1970 纪元的天数再按单元逐级换算W 除以 7负天数时向负无穷取整(days - 6) / 7、D 直接取天数、h 乘 24 加小时、m 再乘 60 加分钟……以此类推到阿秒fs与as单元的源码注释值得留意fs只覆盖约 2.6 小时as只覆盖约 9.2 秒——这是 int64 容量在极细粒度下的固有限制最后除以乘数meta-num负数时使用向负无穷取整(ret - num 1) / num保证与//语义一致。关于溢出文档指出当meta-num很大时可能发生整数溢出源码中TODO: If meta-num is really big, there could be overflow的注释与之呼应。与之相对的逆方向转换NpyDatetime_ConvertDatetime64ToDatetimeStruct则在实现中通过_datetime_scale_with_overflow_check见 numpy/_core/src/multiarray/_datetime.h在缩放num时检查溢出溢出会设置PyExc_OverflowError并返回 -1。2. NpyDatetime_ConvertDatetime64ToDatetimeStructint64 计数 → 分解结构体int NpyDatetime_ConvertDatetime64ToDatetimeStruct( PyArray_DatetimeMetaData *meta, npy_datetime dt, npy_datetimestruct *out)反向转换把meta单元下的 datetime 计数展开为分解结构体。返回 0 成功、-1 失败。实现见 numpy/_core/src/multiarray/datetime.c。实现的细节要点输出先memset清零并初始化为1970-01-01dt NPY_DATETIME_NAT时置out-year NPY_DATETIME_NAT返回base NPY_FR_GENERIC时报错——普通 datetime 值不能使用泛型单元只有 NaT 例外该规则同样存在于ConvertDatetimeStructToDatetime64中先按meta-num放大计数含溢出检查再按单元拆分Y 直接1970 dtM 通过extract_unit_64(dt, 12)同时得到年与月W/D 通过set_datetimestruct_days设置日历日期更细的单元用逐级extract_unit_64提取天数、小时、分钟、秒、微秒……源码特别提醒“对于负值/与%运算符必须小心处理”因此extract_unit_64承担了向负无穷取整的语义。3. NpyDatetime_ConvertPyDateTimeToDatetimeStructPython datetime/date → 分解结构体int NpyDatetime_ConvertPyDateTimeToDatetimeStruct( PyObject *obj, npy_datetimestruct *out, NPY_DATETIMEUNIT *out_bestunit, int apply_tzinfo)检测并把 Python 的datetime.datetime或datetime.date对象转换为npy_datetimestruct。返回值约定为-1 表示错误0 表示成功1未设置异常表示 obj 缺少所需的 date/datetime 属性。参数语义out_bestunit给出一个建议单元——如果对象是datetime.date则建议NPY_FR_D日如果是datetime.datetime则建议NPY_FR_us微秒Python 原生精度apply_tzinfo为 1 时使用对象的 tzinfo 转换到 UTC 时间为 0 时返回本地时间。实现numpy/_core/src/multiarray/datetime.c分为两条路径快路径对精确的datetime.date/datetime.datetime实例直接用 Python datetime C API 宏PyDateTime_GET_YEAR、PyDateTime_DATE_GET_HOUR等从 C 结构体读取字段避免属性访问开销鸭子类型回退路径对 date/datetime 子类及其他暴露 date 属性的对象逐属性读取year/month/day/hour/minute/second/microsecond。此处还处理了特殊情形像 pandas 的 NaT 这类以 NaN 浮点暴露 year 的对象会被识别为 NaTout-year NPY_DATETIME_NAT建议单元NPY_FR_GENERIC而不是报TypeError。随后执行合法性校验月份必须在 1–12、天数不能超过_days_per_month_table[isleap][month-1]闰年表由is_leapyear决定小时 24、分钟与秒 60、微秒 1000000。若启用apply_tzinfo且对象带时区会触发一个UserWarning“no explicit representation of timezones available for np.datetime64”因为np.datetime64本身不保存时区信息。4. NpyDatetime_ParseISO8601DatetimeISO 8601 字符串 → 分解结构体int NpyDatetime_ParseISO8601Datetime( char const *str, Py_ssize_t len, NPY_DATETIMEUNIT unit, NPY_CASTING casting, npy_datetimestruct *out, NPY_DATETIMEUNIT *out_bestunit, npy_bool *out_special)解析“几乎标准”的 ISO 8601 日期字符串实现见 numpy/_core/src/multiarray/datetime_strings.c。str必须是 NULL 结尾的字符串len为其长度。返回 0 成功、-1 失败。文档明确列出的与标准 ISO 8601 的差异这些差异都有源码可查20100312被解析为年份 20100312而非等价的2010-03-12日期中的-分隔符不是可选的只有秒可以带小数点且最多 18 位小数对应最大阿秒精度——实测中np.datetime64(1970-01-01T00:00:02.123456789012345678)会得到M8[as]类型见 numpy/_core/tests/test_datetime.py日期与时间之间既可用 ISO 标准的T也可用 空格两者等价暂不支持YYYY-DDD年-年内第几天与YYYY-WwwISO 周格式不支持闰秒此类情况下秒值为 60不支持将24:00:00作为次日午夜的同义词额外接受特殊值NaTNot-a-Time、Today本地时间的当天、NowUTC 的当前时间。参数详解unit若单元未知应传-1即NPY_FR_ERROR否则传将使用的目标单元casting控制从字符串探测出的单元到unit参数的允许转换方式NPY_CASTING枚举如NPY_SAFE_CASTING、NPY_UNSAFE_CASTING。当unit ! NPY_FR_ERROR且!can_cast_datetime64_units(bestunit, unit, casting)时会抛出TypeError消息形如Cannot parse ... as unit ... using casting rule ...out填充解析出的日期时间out_bestunit根据字符串携带的分辨率给出建议单元NaT 时为 -1out_special解析到today、now、空字符串或NaT时置 1。其中today建议单元为Dnow建议单元为sNaT建议单元为Y。源码中的特殊值处理逻辑非常细致空字符串与大小写变体的nat都映射为 NaTout-year NPY_DATETIME_NAT建议单元为NPY_FR_GENERIC若目标单元为NPY_FR_GENERIC而输入不是 NaT直接报ValueError“Cannot create a NumPy datetime other than NaT with generic units”today通过time()get_localtime()取本地时间当天日期并刻意设计为若强制转换到时间单元则按 UTC 午夜计——这样datetime64[D]打印出的就是用户预期的日期不会因时区导致前后偏移一天源码注释详细说明了这一设计动机now取time()的当前 UTC 时间秒级分辨率建议单元为NPY_FR_s随后直接调用NpyDatetime_ConvertDatetime64ToDatetimeStruct展开为结构体年份解析支持可选的/-符号负年份并跳过前导空白解析完成年月日后通过is_leapyear校验 2 月天数等合法性。Python 侧的对应行为可在 numpy/_core/tests/test_datetime.py 中看到验证np.datetime64(today).dtype为M8[D]np.datetime64(now).dtype为M8[s]np.datetime64(datetime.date(...))为M8[D]np.datetime64(datetime.datetime(...))为M8[us]——与out_bestunit的建议完全一致。5. NpyDatetime_GetDatetimeISO8601StrLen计算字符串缓冲长度int NpyDatetime_GetDatetimeISO8601StrLen(int local, NPY_DATETIMEUNIT base)返回在给定 local 时间与单元设置下把 datetime 对象转换为字符串所需使用的字符串长度。构造字符串缓冲时必须先用它计算长度再把结果交给NpyDatetime_MakeISO8601Datetime。实现numpy/_core/src/multiarray/datetime_strings.c通过 switch 逐级累加各单元段落的字符数NPY_FR_ERROR未提供单元返回最大长度常量NPY_DATETIME_MAX_ISO8601_STRLEN定义为21 3*5 1 3*6 6 1即最坏情况64 位年份 21 字符 月/日 3×5 分隔 时间 3×6 时区 6 结尾 1NPY_FR_GENERIC返回 4NaT 3 字符 NULL 终止符单元从NPY_FR_as逐级向上加 3 字符如fs加###、us加###、ms加.###、s加:##、h加T##、D加-##……直到NPY_FR_Y加 21 字符的 64 位年份若base NPY_FR_hlocal 模式再加 5 字符####/-####否则加 1 字符Z最后加 1 字符的 NULL 终止符。也就是说该函数保证返回的缓冲大小足以容纳对应单元的全部输出包括时区后缀与终止符。6. NpyDatetime_MakeISO8601Datetime分解结构体 → ISO 8601 字符串int NpyDatetime_MakeISO8601Datetime( npy_datetimestruct *dts, char *outstr, npy_intp outlen, int local, int utc, NPY_DATETIMEUNIT base, int tzoffset, NPY_CASTING casting)把npy_datetimestruct转换为几乎标准的 ISO 8601 NULL 结尾字符串。若字符串恰好填满缓冲区则省略 NULL 终止符并仍返回成功——这正是上一条函数要先计算精确长度的原因。返回 0 成功、-1 失败例如输出缓冲区过短。实现见 numpy/_core/src/multiarray/datetime_strings.c。与 ISO 8601 的差异有两处输出NaT字符串年份位数至少 4 位而非严格 4 位例如大年份不会因为固定 4 位宽度而截断。参数语义local非零输出本地时间并带-####时区偏移local为零且utc非零输出以Z结尾表示 UTC两者都不满足默认不附加任何时区信息base限制输出到该单元设为 -1 时自动探测——实现中通过lossless_unit_from_datetimestructnumpy/_core/src/multiarray/datetime_strings.c找到“最大的、值为非零且更低层全为零的单元”例如as % 1000 ! 0则选NPY_FR_as否则若as ! 0选NPY_FR_fs……逐级上推。自动探测时还有两条人性化规则若启用 local 且探测单元低于分钟或探测到小时h则至少提升到分钟精度NPY_FR_m避免默认输出把小时与分钟拆开若探测单元低于天D则提升到天避免默认把日期拆成更细的字段tzoffset仅当local启用且tzoffset ! -1时生效作为手动指定本地时区偏移单位分钟覆盖系统时区casting控制是否允许通过截断到更粗单元而丢失数据。它与local有一个微妙交互要以本地时间形式输出日期单元字符串casting 必须是 unsafe因为本地时间换算必然涉及时分秒属于“有损”转换。另外NpyDatetime_MakeISO8601Datetime对本地时间有一个跨平台限制当年份早于 1970 或大于等于 10000 且未手动指定tzoffset时会强制关闭 local 模式local 0。源码注释说明这是因为 Windows 平台对 1970 之前的localtime调用会失败详见 numpy/_core/src/multiarray/datetime_strings.c 对get_localtime的平台限制说明为跨平台一致而统一禁用该限制只影响字符串输出是否带时区不影响时间值的准确性。转换函数在实际代码中的调用链上述函数并非孤立存在它们是 NumPy 自身转换管线的基础构件datetime64 标量/数组打印scalartypes.c.src中把标量 datetime 转字符串时先调用NpyDatetime_ConvertDatetime64ToDatetimeStruct展开结构体再用NpyDatetime_MakeISO8601Datetime生成可读文本见 numpy/_core/src/multiarray/scalartypes.c.src数组类型转换castdtype_transfer.c中的 datetime 相关转换器先NpyDatetime_ConvertDatetime64ToDatetimeStructNpyDatetime_ConvertDatetimeStructToDatetime64完成跨单元转换或在 datetime64 与字符串 dtype 之间用NpyDatetime_ParseISO8601Datetime/NpyDatetime_MakeISO8601Datetime来回转换见 numpy/_core/src/multiarray/dtype_transfer.c字符串 dtypeStringDTypenumpy/_core/src/multiarray/stringdtype/casts.cpp同样复用了这一整套函数完成StringDType与datetime64之间的互转见 numpy/_core/src/multiarray/stringdtype/casts.cppnp.datetime64构造datetime.c中convert_pyobject_to_datetime依据对象类型分派——datetime.date/datetime.datetime走NpyDatetime_ConvertPyDateTimeToDatetimeStruct字符串走NpyDatetime_ParseISO8601Datetime再统一由NpyDatetime_ConvertDatetimeStructToDatetime64落成 int64 计数见 numpy/_core/src/multiarray/datetime.c。这套函数同时也是仓库中 numpy/_core/tests/test_datetime.py 庞大测试矩阵的验证对象NaT 在各种单元间的无损传播、date/datetime/today/now 的单元推断、任意精度小数的as解析等都被测试用例逐一锁定。使用注意事项小结基于文档与源码在 C 扩展中使用本 API 时需要注意缓冲区长度调用NpyDatetime_MakeISO8601Datetime前务必先用NpyDatetime_GetDatetimeISO8601StrLen求长度该函数保证包括 NULL 终止符在内的容量但“恰好填满”时输出不含终止符NaT 哨兵int64 表示与分解结构体表示中NaT 都是NPY_DATETIME_NATNPY_MIN_INT64。任何合法的dt * num运算都被刻意设计为不会产生NPY_MIN_INT64避免与 NaT 混淆见_datetime_scale_with_overflow_check的不对称负限说明泛型单元限制NPY_FR_GENERIC只能表示 NaT不能承载实际日期时间单元未知时用NPY_FR_ERROR-1作为“自动探测”的输入标记casting 规则跨单元转换遵循NPY_CASTING语义日期单元与时间单元之间存在“屏障”safe 及更严格时不可跨越见can_cast_datetime64_units的说明本地时间输出还可能要求 unsafe时区处理np.datetime64不存储时区NpyDatetime_ConvertPyDateTimeToDatetimeStruct在apply_tzinfo时把对象转成 UTC 并给出警告NpyDatetime_MakeISO8601Datetime则通过local/utc/tzoffset三个开关控制输出时的时区标注方式。延伸阅读本 API 的权威参考Datetime API数据结构定义ndarraytypes.hPyArray_DatetimeMetaData、npy_datetimestruct、NPY_DATETIMEUNIT核心转换实现datetime.c 与 datetime_strings.c内部辅助声明与溢出保护numpy/_core/src/multiarray/_datetime.h行为验证测试numpy/_core/tests/test_datetime.py赞分享科学计算数据分析【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址https://gitcode.com/gh_mirrors/nu/numpy点击查看免费下载相关推荐StarRocks timestamp() 函数详解将 DATE/DATETIME 表达式转换为 DATETIME 值StarRocks timestamp 函数详解将 DATE/DATETIME 表达式转换为 DATETIME 值 timestamp 是 StarRocks数据库OLAP数据仓库大数据湖仓一体数据分析StarRocks to_iso8601 函数详解将日期时间转换为 ISO 8601 标准字符串StarRocks to_iso8601 函数详解将日期时间转换为 ISO 8601 标准字符串 to_iso8601 是 StarRocks 提供的一个日期数据库OLAP数据仓库大数据湖仓一体数据分析StarRocks time_to_sec 函数详解将 TIME 值转换为秒数的用法、示例与底层实现StarRocks time_to_sec 函数详解将 TIME 值转换为秒数的用法、示例与底层实现 time_to_sec 是 StarRocks 日期时间数据库OLAP数据仓库大数据湖仓一体数据分析上一篇7分钟掌握CAMEL合成数据集从AI社会模拟到代码生成的全流程指南下一篇闪电网络安全与隐私保护Mastering the Lightning Network核心要点创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻