首页
/ CPython datetime C API 详解:在 C 扩展中创建、检查与提取日期时间对象

CPython datetime C API 详解:在 C 扩展中创建、检查与提取日期时间对象

2026-09-05 09:08:19作者:姚月梅Lane

本篇技术指南基于 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):

  1. 源文件中必须包含头文件 Include/datetime.h。注意它不是Include/Python.h 自动包含的,必须显式 #include "datetime.h"
  2. 必须在模块初始化函数中调用宏 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*36000 <= microseconds < 1000000(见 Include/datetime.h)。

一个值得注意的细节:timedatetime 对象有两种分配形态——不带 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.cstatic 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_FromDateInclude/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 字节数据段与 datetimedata[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;
}

需要牢记的适用前提与限制:

  1. PyDateTime_IMPORT 不适用于 subinterpreters 场景;
  2. 3.15 起 PyDateTime_IMPORT 线程安全,而直接读写 PyDateTimeAPI 变量并不安全,应一律经由该宏获取;
  3. PyDateTime_CAPI 结构体字段私有且可变化,跨版本扩展请只依赖 PyDateTime_* 宏与检查/提取 API;
  4. 所有 PyDateTime_*_GET_* 提取宏不做类型检查也不检查 NULL,误用将直接导致未定义行为。

相关可进一步深入的仓库文件:Include/datetime.h(全部宏定义与内存布局)、Modules/_datetimemodule.ccapi 结构体初始化与 capsule 导出)、Modules/_testcapi/datetime.c(C API 各宏的测试用例)以及 Lib/test/datetimetester.py(datetime 的 Python 层测试工具)。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384