Как расширить NumPy
Написание модуля расширения
Хотя объект ndarray предназначен для быстрых вычислений в Python, он также разработан для универсального использования и удовлетворения широкого спектра вычислительных потребностей. В результате, если необходима максимальная скорость, нет замены хорошо написанной, скомпилированной петле, специфичной для вашего приложения и оборудования. Это одна из причин, по которой numpy включает f2py, чтобы обеспечить простой механизм связывания (простого) кода C/C++ и (произвольного) Fortran напрямую с Python. Рекомендуется использовать и улучшать этот механизм. Цель этого раздела — не документировать этот инструмент, а документировать более базовые шаги по написанию модуля расширения, от которого этот инструмент зависит.
Когда модуль расширения написан, скомпилирован и установлен в какой-либо части пути Python (sys.path), код можно импортировать в Python, как если бы это был стандартный файл Python. Он будет содержать объекты и методы, определённые и скомпилированные на языке C. Основные шаги для этого в Python хорошо документированы, и вы можете найти больше информации в документации самого Python, доступной онлайн по адресу www.python.org.
В дополнение к C-API Python существует полный и богатый C-API для NumPy, позволяющий выполнять сложные манипуляции на уровне C. Однако для большинства приложений обычно используется лишь несколько вызовов API. Если всё, что вам нужно сделать, это извлечь указатель на память вместе с некоторыми данными о форме, чтобы передать их другой процедуре вычисления, то вы будете использовать совершенно другие вызовы, чем если вы пытаетесь создать новый тип массивоподобного объекта или добавить новый тип данных для ndarray. В этой главе документируются вызовы API и макросы, которые наиболее часто используются.
Необходимая подпрограмма
Существует ровно одна функция, которая должна быть определена в вашем коде C, чтобы Python мог использовать её как модуль расширения. Функция должна называться init{имя}, где {имя} — имя модуля в Python. Эта функция должна быть объявлена так, чтобы быть видимой коду за пределами процедуры. Помимо добавления необходимых методов и констант, эта подпрограмма также должна содержать вызовы, такие как import_array() и/или import_ufunc(), в зависимости от используемого C-API. Если забыть эти команды, это проявится как ошибка сегментации (сбой) как только любая подпрограмма C-API будет фактически вызвана. На самом деле, в одном файле может быть несколько функций init{имя}, в этом случае файл определит несколько модулей. Однако есть некоторые хитрости, чтобы это работало правильно, и они здесь не рассматриваются.
Минимальный init{name} метод выглядит так:
PyMODINIT_FUNC
init{name}(void)
{
(void)Py_InitModule({name}, mymethods);
import_array();
}
mymethods должен быть массивом (обычно статически объявленным) структур PyMethodDef, которые содержат имена методов, фактические функции C, переменную, указывающую, использует ли метод ключевые аргументы, и строки документации. Они описаны в следующем разделе. Если вы хотите добавить константы в модуль, сохраните возвращённое значение из Py_InitModule, которое является объектом модуля. Наиболее общий способ добавления элементов в модуль — получить словарь модуля с помощью PyModule_GetDict(модуль). С помощью словаря модуля вы можете вручную добавить всё, что захотите, в модуль. Более простой способ добавить объекты в модуль — использовать один из трёх дополнительных вызовов Python C-API, которые не требуют отдельного извлечения словаря модуля. Они документированы в документации Python, но повторяются здесь для удобства:
-
int PyModule_AddObject(PyObject* module, char* name, PyObject* value)
-
int PyModule_AddIntConstant(PyObject* module, char* name, long value)
-
int PyModule_AddStringConstant(PyObject* module, char* name, char* value) -
Все три эти функции требуют объект модуля (возвращаемое значение Py_InitModule). Имя — строка, которая маркирует значение в модуле. В зависимости от вызываемой функции, аргумент значение является либо общим объектом (
PyModule_AddObjectзаимствует ссылку на него), либо константой целого числа, либо константой строки.
Определение функций
Вторым аргументом, переданным в функцию Py_InitModule, является структура, которая упрощает определение функций в модуле. В приведённом выше примере структура mymethods была определена ранее в файле (обычно сразу перед подпрограммой init{имя}) для:
static PyMethodDef mymethods[] = {
{ nokeywordfunc,nokeyword_cfunc,
METH_VARARGS,
Doc string},
{ keywordfunc, keyword_cfunc,
METH_VARARGS|METH_KEYWORDS,
Doc string},
{NULL, NULL, 0, NULL} /* Sentinel */
}
Каждый элемент массива mymethods — это структура PyMethodDef, содержащая 1) имя Python, 2) функцию C, которая реализует функцию, 3) флаги, указывающие, принимаются ли ключевые аргументы для этой функции, и 4) строку документации для функции. Для одного модуля может быть определено любое количество функций путём добавления дополнительных элементов в эту таблицу. Последний элемент должен быть нулевым, как показано, чтобы служить индикатором завершения. Python ищет этот элемент, чтобы узнать, что все функции модуля определены.
Последнее, что необходимо сделать для завершения модуля расширения, — написать код, который выполняет желаемые функции. Существует два типа функций: те, которые не принимают ключевые аргументы, и те, которые принимают.
Функции без ключевых аргументов
Функции, которые не принимают ключевые аргументы, должны быть написаны так:
static PyObject*
nokeyword_cfunc (PyObject *dummy, PyObject *args)
{
/* convert Python arguments */
/* do function */
/* return something */
}
Аргумент dummy в этом контексте не используется и может быть безопасно пропущен. Аргумент args содержит все аргументы, переданные в функцию в виде кортежа. Вы можете делать всё, что хотите в этот момент, но обычно самый простой способ управления входными аргументами — это вызвать PyArg_ParseTuple (args, строка_формата, адреса_переменных_C…) или PyArg_UnpackTuple (кортеж, “имя”, мин, макс, …). Хорошее описание того, как использовать первую функцию, содержится в справочнике Python C-API в разделе 5.5 (Парсинг аргументов и построение значений). Обратите особое внимание на формат «O&», который использует функции-конвертеры для перехода между Python-объектом и C-объектом. Все остальные функции форматирования можно (в основном) рассматривать как частные случаи этого общего правила. В NumPy C-API определены несколько функций-конвертеров, которые могут быть полезны. В частности, функция PyArray_DescrConverter очень полезна для поддержки произвольного указания типа данных. Эта функция преобразует любой допустимый Python-объект типа данных в объект PyArray_Descr *. Помните, что нужно передать адрес переменных C, которые должны быть заполнены.
Есть много примеров использования PyArg_ParseTuple во всём исходном коде NumPy. Стандартное использование выглядит так:
PyObject *input;
PyArray_Descr *dtype;
if (!PyArg_ParseTuple(args, "OO&", &input,
PyArray_DescrConverter,
&dtype)) return NULL;
Важно помнить, что при использовании формата «O» вы получаете заимствованную ссылку на объект. Однако функции-конвертеры обычно требуют некоторого управления памятью. В этом примере, если преобразование успешно, dtype будет содержать новую ссылку на объект PyArray_Descr *, а input — заимствованную ссылку. Поэтому, если это преобразование было смешано с другим преобразованием (скажем, в целое число) и преобразование типа данных было успешным, но преобразование целого числа потерпело неудачу, то вам необходимо будет освободить счётчик ссылок на объект типа данных перед возвратом. Типичный способ сделать это — установить dtype в NULL перед вызовом PyArg_ParseTuple, а затем использовать Py_XDECREF для dtype перед возвратом.
После обработки входных аргументов записывается код, который фактически выполняет работу (вероятно, вызывая другие функции по мере необходимости). Последний шаг функции C — вернуть что-то. Если возникла ошибка, то должен быть возвращён NULL (убедившись, что ошибка действительно была установлена). Если ничего не должно быть возвращено, то нужно увеличить Py_None и вернуть его. Если должен быть возвращён один объект, то он возвращается (убедившись, что у вас есть ссылка на него). Если должны быть возвращены несколько объектов, то нужно вернуть кортеж. Функция Py_BuildValue (строка_формата, переменные_C…) легко позволяет создавать кортежи Python-объектов из C-переменных. Обратите особое внимание на разницу между ‘N’ и ‘O’ в строке формата, иначе вы можете легко создать утечку памяти. Формат ‘O’ увеличивает счётчик ссылок на соответствующую переменную C PyObject *, в то время как формат ‘N’ заимствует ссылку на соответствующую переменную C PyObject *. Вы должны использовать ‘N’, если вы уже создали ссылку на объект и просто хотите предоставить эту ссылку в кортеж. Вы должны использовать ‘O’, если у вас есть только заимствованная ссылка на объект и необходимо создать её, чтобы предоставить её в кортеж.
Функции с ключевыми аргументами
Эти функции очень похожи на функции без ключевых аргументов. Единственное отличие состоит в том, что сигнатура функции:
static PyObject*
keyword_cfunc (PyObject *dummy, PyObject *args, PyObject *kwds)
{
...
}
Аргумент kwds содержит словарь Python, ключами которого являются имена ключевых аргументов, а значениями — соответствующие значения ключевых аргументов. Этот словарь можно обработать любым удобным способом. Однако, проще всего заменить функцию PyArg_ParseTuple (args, format_string, addresses…) вызовом PyArg_ParseTupleAndKeywords (args, kwds, format_string, char *kwlist[], addresses…). Параметр kwlist этой функции представляет собой массив строк, завершаемых NULL, содержащий ожидаемые ключевые аргументы. Должна быть одна строка для каждого элемента в format_string. Использование этой функции вызовет TypeError, если переданы неверные ключевые аргументы.
Для получения дополнительной помощи по этой функции обратитесь к разделу 1.8 (Ключевые параметры для функций расширения) руководства по расширению и интеграции в документации Python.
Счётчик ссылок
Самая большая сложность при написании модулей расширения — это счётчик ссылок. Это важная причина популярности f2py, weave, Cython, ctypes и т. д…. Если неправильно обработать счётчик ссылок, могут возникнуть проблемы, начиная от утечек памяти и заканчивая ошибками сегментации. Единственная стратегия, которую я знаю для правильной обработки счётчиков ссылок, — это упорный труд. Во-первых, вы заставляете себя понимать, что каждая переменная Python имеет счётчик ссылок. Затем, вы точно понимаете, что делает каждая функция со счётчиком ссылок ваших объектов, чтобы правильно использовать DECREF и INCREF при необходимости. Счётчик ссылок может действительно проверить количество терпения и старательности, которые вы проявляете в программировании. Несмотря на мрачную картину, большинство случаев обработки счётчика ссылок довольно просты, а наиболее распространённая трудность — это отсутствие использования DECREF для объектов перед преждевременным выходом из процедуры из-за какой-либо ошибки. На втором месте — распространённая ошибка — не владеть ссылкой на объект, который передаётся функции или макросу, который собирается украсть ссылку ( например, PyTuple_SET_ITEM, и большинство функций, принимающих PyArray_Descr объекты).
Обычно вы получаете новую ссылку на переменную при её создании или когда она является результатом некоторой функции (есть некоторые заметные исключения, например, получение элемента из кортежа или словаря). Когда вы владеете ссылкой, вы несёте ответственность за то, чтобы Py_DECREF (var) вызывался, когда переменная больше не нужна (и ни одна другая функция не «украла» её ссылку). Кроме того, если вы передаёте объект Python функции, которая «украдёт» ссылку, то вы должны убедиться, что владеете ей (или использовать Py_INCREF для получения своей собственной ссылки). Вы также встретите понятие заимствования ссылки. Функция, которая заимствует ссылку, не изменяет счётчик ссылок объекта и не ожидает «держать» ссылку. Она просто временно использует объект. Когда вы используете PyArg_ParseTuple или PyArg_UnpackTuple, вы получаете заимствованную ссылку на объекты в кортеже и не должны изменять их счётчик ссылок внутри вашей функции. Со временем вы научитесь правильно обрабатывать счётчик ссылок, но вначале это может быть сложно.
Одним из распространённых источников ошибок счётчика ссылок является функция Py_BuildValue. Внимательно обратите внимание на разницу между символом формата ‘N’ и символом формата ‘O’. Если вы создаёте новый объект в вашей подпрограмме (например, массив вывода), и передаёте его обратно в кортеже возвращаемых значений, то вам, скорее всего, следует использовать символ формата ‘N’ в Py_BuildValue. Символ ‘O’ увеличит счётчик ссылок на единицу. Это оставит вызывающую сторону с двумя счётчиками ссылок для совершенно нового массива. Когда переменная удаляется и счётчик ссылок уменьшается на единицу, всё ещё будет дополнительный счётчик ссылок, и массив никогда не будет освобождён. У вас будет утечка памяти, вызванная обработкой счётчика ссылок. Использование символа ‘N’ предотвратит эту ситуацию, так как он вернёт вызывающей стороне объект (внутри кортежа) с одиночным счётчиком ссылок.
Работа с объектами массивов
Большинству модулей расширения NumPy потребуется доступ к памяти объекта ndarray (или одного из его подклассов). Самый простой способ сделать это не требует от вас глубокого знания внутренней структуры NumPy. Метод заключается в
-
Убедитесь, что вы имеете дело с хорошо организованным массивом (выровненным, в байтовом порядке машины и односегментным) правильного типа и количества измерений.
- Преобразуя его из некоторого объекта Python с помощью
PyArray_FromAnyили макроса, построенного на его основе. - Построив новый ndarray нужной формы и типа с помощью
PyArray_NewFromDescrили более простого макроса или функции, основанной на нём.
- Преобразуя его из некоторого объекта Python с помощью
- Получите форму массива и указатель на его фактические данные.
- Передайте информацию о данных и форме в подпрограмму или другой раздел кода, который фактически выполняет вычисление.
- Если вы пишете алгоритм, я рекомендую использовать информацию о шаге, содержащуюся в массиве, для доступа к элементам массива (макросы
PyArray_GetPtrделают это без проблем). Затем вы можете ослабить свои требования, чтобы не принуждать односегментный массив и копирование данных, которое может возникнуть.
Каждая из этих подтем освещается в следующих подразделах.
Преобразование произвольного последовательного объекта
Основная процедура получения массива из любого объекта Python, который может быть преобразован в массив, — это PyArray_FromAny. Эта функция очень гибкая с многими входными аргументами. Несколько макросов делают её использование проще. PyArray_FROM_OTF можно считать наиболее полезным из этих макросов для наиболее распространённых случаев использования. Он позволяет преобразовать произвольный объект Python в массив определённого встроенного типа данных ( например, float), одновременно определяя определённый набор требований ( например, непрерывный, выровненный и доступный для записи). Синтаксис
Возвращает ndarray из любого объекта Python, obj, который может быть преобразован в массив. Количество измерений в возвращённом массиве определяется объектом. Желаемый тип данных возвращённого массива предоставляется в typenum, которое должно быть одним из перечисленных типов. Требования к возвращённому массиву могут быть любой комбинацией стандартных флагов массива. Каждый из этих аргументов поясняется более подробно ниже. В случае успеха вы получаете новую ссылку на массив. В случае неудачи возвращается NULL и устанавливается исключение.
obj
Объект может быть любым объектом Python, преобразуемым в ndarray. Если объект уже является (подклассом) ndarray, удовлетворяющим требованиям, то возвращается новая ссылка. В противном случае строится новый массив. Содержимое obj копируется в новый массив, если не используется интерфейс массива, чтобы данные не нужно было копировать. Объекты, которые могут быть преобразованы в массив, включают: 1) любые вложенные последовательные объекты, 2) любые объекты, использующие интерфейс массива, 3) любые объекты с методом __array__ (который должен возвращать ndarray), и 4) любые скалярные объекты (превращаются в массив нулевой размерности). Подклассы ndarray, которые иначе соответствуют требованиям, будут переданы. Если вы хотите гарантировать базовый класс ndarray, используйте NPY_ARRAY_ENSUREARRAY в флаге требований. Копия создаётся только при необходимости. Если вы хотите гарантировать копию, передайте NPY_ARRAY_ENSURECOPY в флаг требований.
typenum
Один из перечисленных типов или NPY_NOTYPE, если тип данных должен определяться из самого объекта. Можно использовать C-базовые имена:
NPY_BOOL, NPY_BYTE, NPY_UBYTE, NPY_SHORT, NPY_USHORT, NPY_INT, NPY_UINT, NPY_LONG, NPY_ULONG, NPY_LONGLONG, NPY_ULONGLONG, NPY_DOUBLE, NPY_LONGDOUBLE, NPY_CFLOAT, NPY_CDOUBLE, NPY_CLONGDOUBLE, NPY_OBJECT.
В качестве альтернативы, можно использовать имена с указанием разрядности, поддерживаемые на платформе. Например:
NPY_INT8, NPY_INT16, NPY_INT32, NPY_INT64, NPY_UINT8, NPY_UINT16, NPY_UINT32, NPY_UINT64, NPY_FLOAT32, NPY_FLOAT64, NPY_COMPLEX64, NPY_COMPLEX128.
Объект будет преобразован к нужному типу только в том случае, если это можно сделать без потери точности. В противном случае вернётся NULL и будет возбуждено исключение. Используйте NPY_ARRAY_FORCECAST в флагах требований, чтобы переопределить это поведение.
требования
Модель памяти для ndarray допускает произвольные шаги в каждом измерении для перехода к следующему элементу массива. Однако часто необходимо взаимодействовать с кодом, ожидающим C-непрерывное или Fortran-непрерывное расположение памяти. Кроме того, ndarray может быть невыровненным (адрес элемента не является целым кратным размеру элемента), что может привести к сбою программы (или, по крайней мере, к более медленной работе), если вы попытаетесь обратиться к указателю в данные массива. Обе эти проблемы можно решить, преобразовав объект Python в массив, который более «подобает» для вашего конкретного использования.
Флаги требований позволяют указать, какой тип массива приемлем. Если переданный объект не удовлетворяет этим требованиям, создаётся копия, чтобы возвращаемый объект удовлетворял требованиям. Эти ndarray могут использовать очень общий указатель на память. Этот флаг позволяет указать желаемые свойства возвращаемого объекта массива. Все флаги подробно описаны в главе API. Наиболее часто необходимыми флагами являются NPY_ARRAY_IN_ARRAY, NPY_OUT_ARRAY и NPY_ARRAY_INOUT_ARRAY:
Этот флаг полезен для массивов, которые должны быть непрерывными в порядке C и выровненными. Такие массивы обычно являются входными массивами для некоторого алгоритма.
Этот флаг полезен для указания массива, который непрерывен в порядке C, выровнен и в который можно записывать. Такой массив обычно возвращается как выходной (хотя обычно такие выходные массивы создаются с нуля).
Этот флаг полезен для указания массива, который будет использоваться как для входных, так и для выходных данных. PyArray_ResolveWritebackIfCopy должен быть вызван перед Py_DECREF в конце процедуры интерфейса, чтобы записать временные данные обратно в исходный массив, переданный в функцию. Использование флагов NPY_ARRAY_WRITEBACKIFCOPY или NPY_ARRAY_UPDATEIFCOPY требует, чтобы входной объект уже был массивом (поскольку другие объекты не могут быть автоматически обновлены таким образом). Если произошла ошибка, используйте PyArray_DiscardWritebackIfCopy (объект) для массива с установленными этими флагами. Это сделает базовый массив доступным для записи без копирования содержимого обратно в исходный массив.
Другие полезные флаги, которые могут быть объединены как дополнительные требования:
Принудительно преобразовать в нужный тип, даже если это невозможно без потери информации.
Убедиться, что результирующий массив является копией оригинала.
Убедиться, что результирующий объект является фактическим ndarray, а не подклассом.
Примечание
То, является ли массив байт-переставленным, определяется типом данных массива. Массивы с родным порядком байтов всегда запрашиваются PyArray_FROM_OTF, поэтому флаг NPY_ARRAY_NOTSWAPPED не нужен в аргументе требований. Также невозможно получить байт-переставленный массив с помощью этой функции.
Создание нового ndarray
Часто необходимо создавать новые массивы в коде модуля расширения. Возможно, нужен выходной массив, и вы не хотите, чтобы вызывающий код его предоставлял. Возможно, нужен только временный массив для промежуточных вычислений. В любом случае есть простые способы получить объект ndarray нужного типа данных. Наиболее общей функцией для этого является PyArray_NewFromDescr. Все функции создания массивов используют этот код с многократным использованием. Из-за своей гибкости его использование может быть несколько запутанным. Поэтому существуют более простые формы, которые легче использовать. Эти формы являются частью семейства функций PyArray_SimpleNew, которые упрощают интерфейс, предоставляя значения по умолчанию для распространённых случаев использования.
Доступ к памяти ndarray и обращение к элементам ndarray
Если obj является массивом ndarray (PyArrayObject *), то область данных ndarray указывается указателем void* PyArray_DATA (obj) или указателем char* PyArray_BYTES (obj). Помните, что (вообще говоря) эта область данных может быть не выровнена по типу данных, может представлять данные с переставленными байтами и/или может быть не доступна для записи. Если область данных выровнена и в родном порядке байтов, то способ получения конкретного элемента массива определяется только массивом переменных npy_intp, PyArray_STRIDES (obj). В частности, этот целочисленный массив показывает, сколько байтов необходимо добавить к текущему указателю элемента, чтобы получить следующий элемент в каждом измерении. Для массивов с менее чем 4 измерениями существуют макросы PyArray_GETPTR{k} (obj, …), где {k} — целые числа 1, 2, 3 или 4, которые упрощают использование шагов массива. Аргументы… представляют собой {k} неотрицательных целочисленных индексов в массиве. Например, предположим, что E — это 3-мерный массив ndarray. Указатель (void*) на элемент E[i,j,k] получается как PyArray_GETPTR3 (E, i, j, k).
Как уже объяснялось, массивы, упорядоченные по стилю C, и массивы, упорядоченные по стилю Fortran, имеют особые схемы шагов. Два флага массива (NPY_ARRAY_C_CONTIGUOUS и NPY_ARRAY_F_CONTIGUOUS) указывают, соответствует ли схема шагов данного массива упорядочиванию по стилю C, упорядочиванию по стилю Fortran или ни тому, ни другому. Можно проверить, соответствует ли схема шагов стандартному порядку C или Fortran, используя PyArray_IS_C_CONTIGUOUS (obj) и PyArray_ISFORTRAN (obj) соответственно. Большинство сторонних библиотек ожидают упорядоченные массивы. Однако часто несложно поддерживать общую схему шагов. Я рекомендую использовать информацию о шагах в вашем собственном коде всякий раз, когда это возможно, и оставлять требования к одному сегменту для обертывания стороннего кода. Использование информации о шагах, предоставленной с ndarray, а не требование упорядоченных шагов, уменьшает копирование, которое в противном случае должно быть выполнено.
Пример
Следующий пример показывает, как можно написать оболочку, которая принимает два входных аргумента (которые будут преобразованы в массив) и выходной аргумент (который должен быть массивом). Функция возвращает None и обновляет выходной массив. Обратите внимание на обновленное использование семантики WRITEBACKIFCOPY для NumPy v1.14 и выше
static PyObject *
example_wrapper(PyObject *dummy, PyObject *args)
{
PyObject *arg1=NULL, *arg2=NULL, *out=NULL;
PyObject *arr1=NULL, *arr2=NULL, *oarr=NULL;
if (!PyArg_ParseTuple(args, "OOO!", &arg1, &arg2,
&PyArray_Type, &out)) return NULL;
arr1 = PyArray_FROM_OTF(arg1, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY);
if (arr1 == NULL) return NULL;
arr2 = PyArray_FROM_OTF(arg2, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY);
if (arr2 == NULL) goto fail;
#if NPY_API_VERSION >= 0x0000000c
oarr = PyArray_FROM_OTF(out, NPY_DOUBLE, NPY_ARRAY_INOUT_ARRAY2);
#else
oarr = PyArray_FROM_OTF(out, NPY_DOUBLE, NPY_ARRAY_INOUT_ARRAY);
#endif
if (oarr == NULL) goto fail;
/* code that makes use of arguments */
/* You will probably need at least
nd = PyArray_NDIM(<..>) -- number of dimensions
dims = PyArray_DIMS(<..>) -- npy_intp array of length nd
showing length in each dim.
dptr = (double *)PyArray_DATA(<..>) -- pointer to data.
If an error occurs goto fail.
*/
Py_DECREF(arr1);
Py_DECREF(arr2);
#if NPY_API_VERSION >= 0x0000000c
PyArray_ResolveWritebackIfCopy(oarr);
#endif
Py_DECREF(oarr);
Py_INCREF(Py_None);
return Py_None;
fail:
Py_XDECREF(arr1);
Py_XDECREF(arr2);
#if NPY_API_VERSION >= 0x0000000c
PyArray_DiscardWritebackIfCopy(oarr);
#endif
Py_XDECREF(oarr);
return NULL;
}
© 2005–2020 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.18/user/c-info.how-to-extend.html