CPython datetime C API 详解:在 C 扩展中创建、检查与提取日期时间对象
本篇技术指南基于 CPython 官方文档 Doc/c-api/datetime.rst 系统讲解 datetime C API:如何在 C 扩展模块中通过 PyDateTime_IMPORT 导入 C API 结构体、用构造宏创建 date/datetime/time/timedelta/timezone 对象、用检查宏做类型判断,以及用提取宏读取对象内部字段。读完本文后,你将能够编写正确处理日期时间的 C 扩展,并从源码层面理解这些宏背后的 capsule 导入机制与内存布局。
使用前提:包含头文件并调用 PyDateTime_IMPORT
datetime 模块提供了一组日期与时间对象。在 C 扩展中使用这些 API 之前,必须满足两个前提(来自 Doc/c-api/datetime.rst):
- 源文件中必须包含头文件 Include/datetime.h。注意它不是被 Include/Python.h 自动包含的,必须显式
#include "datetime.h"; - 必须在模块初始化函数中调用宏
PyDateTime_IMPORT。该宏会把一个指向 C 结构体的指针存入静态变量PyDateTimeAPI,后续所有PyDateTime_*宏都依赖这个指针。
PyDateTime_IMPORT 的使用模式如下(文档推荐的错误检查方式):
PyDateTime_IMPORT;
if (PyErr_Occurred()) { /* cleanup */ }
- 成功时,
PyDateTimeAPI指向已填充的PyDateTime_CAPI结构体; - 失败时,
PyDateTimeAPI被置为NULL并设置异常,调用方必须通过PyErr_Occurred()检查是否出错。
两个重要的版本行为变化(以当前仓库文档为准):
- 3.15 起线程安全:
PyDateTime_IMPORT现在是线程安全的; - 不兼容子解释器(subinterpreters):文档明确警告该宏不适用于 subinterpreters 场景。
底层实现:capsule 导入与原子操作
从 Include/datetime.h 可以看到,PyDateTime_IMPORT 实际上展开为内联函数 _PyDateTime_IMPORT(),其工作机制是:
static inline PyDateTime_CAPI *
_PyDateTime_IMPORT(void) {
PyDateTime_CAPI *val = (PyDateTime_CAPI *)_Py_atomic_load_ptr(&PyDateTimeAPI);
if (val == NULL) {
PyDateTime_CAPI *capi = (PyDateTime_CAPI *)PyCapsule_Import(
PyDateTime_CAPSULE_NAME, 0);
if (capi != NULL) {
/* if the compare exchange fails then in that case
another thread would have initialized it */
_Py_atomic_compare_exchange_ptr(&PyDateTimeAPI, &val, (void *)capi);
return capi;
}
}
return val;
}
#define PyDateTime_IMPORT _PyDateTime_IMPORT()
这解释了 3.15 线程安全改动的实现:宏先通过原子读检查 PyDateTimeAPI 是否已初始化;若为 NULL,则用 PyCapsule_Import 按 capsule 名称导入 C API 结构体,再用原子比较交换(CAS)写回全局变量——即使两个线程同时竞争,也只会有一方成功写入,另一方返回竞争者已初始化的指针。这个 capsule 由 datetime 扩展模块在加载时创建:Modules/_datetimemodule.c 中的 _datetime_exec() 通过 PyCapsule_New(capi, PyDateTime_CAPSULE_NAME, NULL) 创建胶囊对象,并以属性名 datetime_CAPI 挂到模块上。
对应地,文档中的三个核心符号为:
PyDateTime_CAPI:包含 datetime C API 所有字段的结构体类型(见 Include/datetime.h)。文档强调其字段是私有的、随时可能变化,不要直接使用,应优先使用PyDateTime_*API;PyDateTimeAPI:动态分配的、包含 datetime C API 的指针变量,只有在PyDateTime_IMPORT成功后才可用。3.15 起文档特别提醒:不应直接访问该变量(直接访问不是线程安全的),应改用PyDateTime_IMPORT获取;- 结构体中实际包含的内容:5 个类型对象指针(
DateType/DateTimeType/TimeType/DeltaType/TZInfoType)、UTC 单例TimeZone_UTC、6 个构造器函数指针、2 个 DB API 构造器函数指针,以及 2 个 PEP 495(fold)构造器函数指针。
对象类型与类型对象
四个 PyObject 子类型
文档定义了 4 个表示不同日期时间对象的 PyObject 子类型,它们的真实内存布局可在 Include/datetime.h 中查到:
| C 类型 | 对应的 Python 对象 | 源码中的结构体定义 |
|---|---|---|
PyDateTime_Date |
datetime.date |
Include/datetime.h |
PyDateTime_DateTime |
datetime.datetime |
Include/datetime.h |
PyDateTime_Time |
datetime.time |
Include/datetime.h |
PyDateTime_Delta |
datetime.timedelta(两个 datetime 值之差) |
Include/datetime.h |
从源码结构看,这些类型的内存布局是高度紧凑的:date/time/datetime 把字段打包进连续字节(大端),例如 year 占 2 字节(1-9999)、month/day/hour/minute/second 各 1 字节、usecond 占 3 字节(0-999999),对应宏 _PyDateTime_DATE_DATASIZE(4 字节)、_PyDateTime_TIME_DATASIZE(6 字节)、_PyDateTime_DATETIME_DATASIZE(10 字节),见 Include/datetime.h 的布局注释。而 timedelta 直接以三个 int 成员存储,且维持两个不变式:0 <= seconds < 24*3600、0 <= microseconds < 1000000(见 Include/datetime.h)。
一个值得注意的细节:time 和 datetime 对象有两种分配形态——不带 tzinfo 成员(相当于 tzinfo == None,但更省内存,如 _PyDateTime_BaseTime/_PyDateTime_BaseDateTime)和带 fold + tzinfo 成员的完整形态;hastzinfo 布尔标志决定访问 tzinfo 字段时走哪条路径。这正是下面 PyDateTime_*_GET_TZINFO 宏用 _PyDateTime_HAS_TZINFO(o) 做条件判断的原因。
五个 PyTypeObject 全局变量
与 Python 层类型一一对应的 PyTypeObject 实例:
| C API 符号 | Python 层等价对象 |
|---|---|
PyDateTime_DateType |
datetime.date |
PyDateTime_DateTimeType |
datetime.datetime |
PyDateTime_TimeType |
datetime.time |
PyDateTime_DeltaType |
datetime.timedelta |
PyDateTime_TZInfoType |
datetime.tzinfo |
这些指针经由 PyDateTimeAPI 结构体暴露,在 Modules/_datetimemodule.c 的 static PyDateTime_CAPI capi 初始化中被填值(.DateType = &PyDateTime_DateType 等)。源码注释特别说明这些类对象需要能存活到解释器之后,因为用户可能持有对它们的借用引用——测试扩展 Modules/_testcapi/datetime.c 也用 PyType_HasFeature(..., Py_TPFLAGS_HEAPTYPE) 断言它们不是堆类型。
UTC 单例
PyObject* PyDateTime_TimeZone_UTC
返回表示 UTC 的 timezone 单例,与 Python 层的 datetime.timezone.utc 是同一个对象(3.7 加入)。在 Include/datetime.h 中它只是一个宏:#define PyDateTime_TimeZone_UTC PyDateTimeAPI->TimeZone_UTC,指向模块内静态的 utc_timezone 对象。
类型检查宏(Type-check macros)
8 个检查宏,规则统一:入参 ob 不得为 NULL;_Check 版本接受精确类型或其子类,_CheckExact 版本只接受精确类型;所有函数总是成功返回(不设置异常)。
| 宏 | 匹配类型 | 是否接受子类 |
|---|---|---|
PyDate_Check(ob) / PyDate_CheckExact(ob) |
PyDateTime_DateType |
是 / 否 |
PyDateTime_Check(ob) / PyDateTime_CheckExact(ob) |
PyDateTime_DateTimeType |
是 / 否 |
PyTime_Check(ob) / PyTime_CheckExact(ob) |
PyDateTime_TimeType |
是 / 否 |
PyDelta_Check(ob) / PyDelta_CheckExact(ob) |
PyDateTime_DeltaType |
是 / 否 |
PyTZInfo_Check(ob) / PyTZInfo_CheckExact(ob) |
PyDateTime_TZInfoType |
是 / 否 |
从 Include/datetime.h 的源码可以确认二者的实现差异:PyDate_Check 使用 PyObject_TypeCheck(op, PyDateTimeAPI->DateType)(即 PyObject_IsInstance 语义,走 MRO),而 PyDate_CheckExact 使用 Py_IS_TYPE(op, PyDateTimeAPI->DateType)(只比较 Py_TYPE(op) 指针)。测试扩展 Modules/_testcapi/datetime.c 中用 MAKE_DATETIME_CHECK_FUNC 宏成对包装了这 10 个函数,供 Lib/test 下的 C API 测试分别验证两类语义。
对象构造宏(Macros to create objects)
构造宏全部以「函数指针间接调用 + 类型对象参数」的形式定义,例如 PyDate_FromDate 在 Include/datetime.h 中展开为 PyDateTimeAPI->Date_FromDate((year), (month), (day), PyDateTimeAPI->DateType),即底层构造器还接收一个 PyTypeObject* 参数,以便将来支持子类化场景;公开宏则固定传入精确类型。
| 宏 | 说明 | 版本 |
|---|---|---|
PyDate_FromDate(year, month, day) |
返回指定的 datetime.date 对象 |
— |
PyDateTime_FromDateAndTime(year, month, day, hour, minute, second, usecond) |
返回指定的 datetime.datetime 对象 |
— |
PyDateTime_FromDateAndTimeAndFold(year, month, day, hour, minute, second, usecond, fold) |
额外指定 fold(PEP 495,用于夏令时回拨歧义时刻) |
3.6 |
PyTime_FromTime(hour, minute, second, usecond) |
返回指定的 datetime.time 对象 |
— |
PyTime_FromTimeAndFold(hour, minute, second, usecond, fold) |
额外指定 fold |
3.6 |
PyDelta_FromDSU(days, seconds, useconds) |
返回 datetime.timedelta 对象。会执行归一化,使结果的 seconds/microseconds 落在 timedelta 文档所述范围内 |
— |
PyTimeZone_FromOffset(offset) |
返回未命名的固定偏移 datetime.timezone 对象,offset 为 timedelta |
3.7 |
PyTimeZone_FromOffsetAndName(offset, name) |
返回带 tzname 的固定偏移 datetime.timezone 对象 |
3.7 |
几个与源码对应的要点:
PyDateTime_FromDateAndTime宏在调用底层函数时固定传入Py_None作为 tzinfo 参数(见 Include/datetime.h),所以它构造出来的始终是 naive datetime;PyDelta_FromDSU的第 4 个参数固定传1,即启用normalize(见 Include/datetime.h),这解释了文档中"归一化被执行"的表述;PyTimeZone_FromOffset等价于PyDateTimeAPI->TimeZone_FromTimeZone((offset), NULL),即 name 传NULL(见 Include/datetime.h)。
一个可运行的用法示例(模式取自测试扩展 Modules/_testcapi/datetime.c 构造 EST 时区的代码):
PyDateTime_IMPORT;
if (PyErr_Occurred()) { return NULL; }
PyObject *offset = PyDelta_FromDSU(0, -18000, 0); /* -5 小时 */
PyObject *name = PyUnicode_FromString("EST");
if (offset == NULL || name == NULL) {
Py_XDECREF(offset);
Py_XDECREF(name);
return NULL;
}
PyObject *est_zone = PyTimeZone_FromOffsetAndName(offset, name);
Py_DECREF(offset);
Py_DECREF(name);
/* 检查 est_zone,必要时 DECREF */
字段提取宏:不检查类型的快速读取
文档对三类提取宏给出了统一的使用契约:参数必须是相应类型的实例(含子类),不得为 NULL,且类型不会被检查——即调用者有责任先用检查宏或其他方式保证类型正确。这与 Include/datetime.h 中宏的实现一致:它们直接做结构体成员/字节读取,没有任何类型判断开销。
date 对象字段
适用于 PyDateTime_Date 实例,包括子类(如 PyDateTime_DateTime,因为 datetime 在 C 层是 date 的子类,date 的 4 字节数据段与 datetime 的 data[0..3] 布局一致):
| 宏 | 返回值 |
|---|---|
PyDateTime_GET_YEAR(o) |
年,正整数 |
PyDateTime_GET_MONTH(o) |
月,1-12 |
PyDateTime_GET_DAY(o) |
日,1-31 |
datetime 对象字段
适用于 PyDateTime_DateTime 实例(含子类):
| 宏 | 返回值 | 版本 |
|---|---|---|
PyDateTime_DATE_GET_HOUR(o) |
0-23 | — |
PyDateTime_DATE_GET_MINUTE(o) |
0-59 | — |
PyDateTime_DATE_GET_SECOND(o) |
0-59 | — |
PyDateTime_DATE_GET_MICROSECOND(o) |
0-999999 | — |
PyDateTime_DATE_GET_FOLD(o) |
0 或 1 | 3.6 |
PyDateTime_DATE_GET_TZINFO(o) |
tzinfo 对象,可能是 None |
3.10 |
GET_TZINFO 的源码实现(Include/datetime.h)依赖前文提到的 hastzinfo 标志:
#define PyDateTime_DATE_GET_TZINFO(o) (_PyDateTime_HAS_TZINFO((o)) ? \
((PyDateTime_DateTime *)(o))->tzinfo : Py_None)
即无时区信息的对象返回 Py_None,而不是解引用不存在的 tzinfo 成员。GET_MICROSECOND 则按大端拼 3 字节:data[7]<<16 | data[8]<<8 | data[9]。
time 对象字段
适用于 PyDateTime_Time 实例(含子类),宏名与 datetime 版本一一对应,只是读取 time 自身的 data 数组(data[0] 即小时,见 Include/datetime.h):
| 宏 | 返回值 | 版本 |
|---|---|---|
PyDateTime_TIME_GET_HOUR(o) |
0-23 | — |
PyDateTime_TIME_GET_MINUTE(o) |
0-59 | — |
PyDateTime_TIME_GET_SECOND(o) |
0-59 | — |
PyDateTime_TIME_GET_MICROSECOND(o) |
0-999999 | — |
PyDateTime_TIME_GET_FOLD(o) |
0 或 1 | 3.6 |
PyDateTime_TIME_GET_TZINFO(o) |
tzinfo 对象,可能是 None |
3.10 |
timedelta 对象字段
适用于 PyDateTime_Delta 实例(含子类),直接读三个 int 成员(Include/datetime.h):
| 宏 | 返回值 | 版本 |
|---|---|---|
PyDateTime_DELTA_GET_DAYS(o) |
天数,-999999999 到 999999999 | 3.3 |
PyDateTime_DELTA_GET_SECONDS(o) |
秒数,0-86399 | 3.3 |
PyDateTime_DELTA_GET_MICROSECONDS(o) |
微秒数,0-999999 | 3.3 |
seconds 的取值范围 0-86399 与 PyDateTime_Delta 结构体注释中的不变式 0 <= seconds < 24*3600 相互印证;负的 timedelta 通过负的 days 表示,seconds/microseconds 恒为非负。
DB API 辅助宏
CPython 文档还为实现 DB API 的模块(如数据库驱动)提供了两个便捷宏:
| 宏 | 说明 |
|---|---|
PyDateTime_FromTimestamp(args) |
给定适合传给 datetime.datetime.fromtimestamp 的参数元组 args,创建并返回新的 datetime.datetime 对象 |
PyDate_FromTimestamp(args) |
给定适合传给 datetime.date.fromtimestamp 的参数元组 args,创建并返回新的 datetime.date 对象 |
从 Include/datetime.h 的实现看,这两个宏把 PyDateTimeAPI 中对应类型对象自身作为 cls 参数传入底层函数(PyDateTimeAPI->DateTime_FromTimestamp((PyObject*)PyDateTimeAPI->DateTimeType, (args), NULL) 等),本质上等价于在 C 层调用 type(args 对应的 fromtimestamp 调用)。
内部符号与完整使用模板
文档最后指出,以下符号虽由 C API 暴露,但应视为内部专用:
PyDateTime_CAPSULE_NAME:传给PyCapsule_Import的 datetime capsule 名称(在 Include/datetime.h 中定义为"datetime.datetime_CAPI",且该名称同时出现在 Modules/_datetimemodule.c 创建 capsule 的调用中)。内部使用专用,对外请使用PyDateTime_IMPORT。
综合以上各节,一个符合文档规范的 C 扩展模块初始化模板为:
#include "Python.h"
#include "datetime.h" /* 不会被 Python.h 自动包含 */
static int
my_module_init(PyObject *module)
{
PyDateTime_IMPORT; /* 模块初始化阶段调用一次 */
if (PyErr_Occurred()) {
return -1; /* 导入失败:PyDateTimeAPI 为 NULL 且设置了异常 */
}
/* 之后即可使用 PyDate_FromDate / PyDateTime_Check / PyDateTime_GET_YEAR 等宏 */
return 0;
}
需要牢记的适用前提与限制:
PyDateTime_IMPORT不适用于 subinterpreters 场景;- 3.15 起
PyDateTime_IMPORT线程安全,而直接读写PyDateTimeAPI变量并不安全,应一律经由该宏获取; PyDateTime_CAPI结构体字段私有且可变化,跨版本扩展请只依赖PyDateTime_*宏与检查/提取 API;- 所有
PyDateTime_*_GET_*提取宏不做类型检查也不检查 NULL,误用将直接导致未定义行为。
相关可进一步深入的仓库文件:Include/datetime.h(全部宏定义与内存布局)、Modules/_datetimemodule.c(capi 结构体初始化与 capsule 导出)、Modules/_testcapi/datetime.c(C API 各宏的测试用例)以及 Lib/test/datetimetester.py(datetime 的 Python 层测试工具)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00