2025, Oct 15 14:48

Как вычитать дни в CPython C‑API: PyDelta_FromDSU и PyNumber_Subtract

Разбираем арифметику datetime в CPython C‑API: создаём timedelta через PyDelta_FromDSU и вычитаем день с PyNumber_Subtract. Пример C‑кода и вызов из ctypes.

Работа с датами на уровне C в CPython зачастую документирована хуже, чем высокоуровневая часть Python. Типичный сценарий — задать одну дату на основе другой: например, если начальная метка времени отсутствует, считать её «на один день раньше конечной». И сделать это, не покидая C‑API Python.

Задача в контексте

Предположим, нативная функция принимает два объекта Python: start и finish. Если start не передан, его следует вычислить как «finish минус один день». Преобразование входов, которые не являются датами, уже предусмотрено, однодневный timedelta создан — не хватает только шага применения этого смещения на уровне C‑API.

/* Пытаемся преобразовать конечную дату, если это ещё не дата */
if (!PyDate_Check(endObj)) {
    tmp = PyDate_FromTimestamp(endObj);
    if (tmp == NULL)
        return PyErr_Format(PyExc_ValueError,
            "%R is not a valid date", endObj);
    endObj = tmp;
}

/* Если начальная дата не указана, считаем её на сутки раньше конечной */
if (beginObj == NULL) {
    PyObject *deltaOne = PyDelta_FromDSU(1, 0, 0);
    /* Нужно получить beginObj из endObj, как‑то применив сдвиг на один день */
}

Что происходит на самом деле

C‑API Python предоставляет конструкторы для типов date и timedelta, но не даёт прямых средств для арифметики с датами. Это не делает задачу невозможной — просто вычисления нужно выражать через общий числовой протокол. На практике это API PyNumber, которое запускает те же операции, что и обычный Python‑код (например, под капотом вызывает __sub__).

Решение: используйте PyNumber API

Создав timedelta через PyDelta_FromDSU, примените смещение с помощью PyNumber_Subtract. Ниже — минимальный пример нативной функции, вызываемой через ctypes в Windows: если start передан, возвращается он же; если start равен None, из finish вычитается один день. Определения datetime в C нужно инициализировать: подключить <datetime.h> и вызвать PyDateTime_IMPORT.

cl /LD /W4 /Ic:\python313\include test.c -link /libpath:c:\python313\libs

#include <Python.h>
#include <datetime.h>

__declspec(dllexport)
PyObject* derive_begin(PyObject* begin_obj, PyObject* end_obj) {
    PyDateTime_IMPORT;
    if (begin_obj == Py_None) {
        PyObject* delta1 = PyDelta_FromDSU(1, 0, 0);
        begin_obj = PyNumber_Subtract(end_obj, delta1);
        Py_XDECREF(delta1);
    }
    return begin_obj;
}

И небольшой драйвер, чтобы показать поведение без написания полноценного модуля-расширения:

import datetime as dt
import ctypes as ct

lib = ct.PyDLL('./test')
derive_begin = lib.derive_begin
derive_begin.argtypes = ct.py_object, ct.py_object
derive_begin.restype = ct.py_object

beg = dt.datetime(2025, 8, 14)
end_ = dt.datetime(2025, 8, 14, 1)

print(f'begin  = {beg}')
print(f'end    = {end_}')
print(derive_begin(beg, end_))   # возвращает begin
print(derive_begin(None, end_))  # возвращает значение на 1 день раньше end

Пример вывода:

begin  = 2025-08-14 00:00:00
end    = 2025-08-14 01:00:00
2025-08-14 00:00:00
2025-08-13 01:00:00

Почему это важно

Нативному коду нередко нужно манипулировать объектами datetime из Python без возврата к собственной C‑логике дат. Связка PyDelta_FromDSU и PyNumber_Subtract удерживает всё внутри объектной модели Python и избавляет от преобразований в struct tm и обратно или от API в стиле mktime. Когда C‑API datetime не предоставляет арифметических помощников, PyNumber служит мостом, вызывая те же операции, что выполняет Python‑код.

Итоги

Если вам нужна арифметика дат в Python C‑API, создавайте объекты timedelta через PyDelta_FromDSU и применяйте их через PyNumber, например PyNumber_Subtract. Подключите <datetime.h>, перед использованием вызовите PyDateTime_IMPORT и следите за временными ссылками, которые создаёте. Если требуется лишь подставлять значения по умолчанию, прагматично решить это на стороне Python до вызова нативной функции, оставив C‑части только основную логику.

Материал основан на вопросе на StackOverflow от Mikhail T. и ответе Mark Tolonen.