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‑части только основную логику.