Как расширить 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{name}, где {name} — имя модуля из Python. Эта функция должна быть объявлена так, чтобы быть видимой коду за пределами этой подпрограммы. Помимо добавления необходимых методов и констант, эта подпрограмма также должна содержать вызовы типа import_array() и/или import_ufunc(), в зависимости от используемого C-API. Пропуск этих команд проявится как неприглядная ошибка сегментации (сбой), как только будет вызвано любое подпрограмма C-API. На самом деле возможно иметь несколько функций init{name} в одном файле, в котором случае этот файл будет определять несколько модулей. Однако для корректной работы существуют некоторые ухищрения, которые здесь не рассматриваются.
Минимальный init{name} метод выглядит следующим образом:
PyMODINIT_FUNC
init{name}(void)
{
(void)Py_InitModule({name}, mymethods);
import_array();
}
Переменная mymethods должна быть массивом (обычно статически объявленным) структур PyMethodDef, содержащих имена методов, фактические C-функции, переменную, указывающую, использует ли метод ключевые аргументы, и строки документации. Они объяснены в следующем разделе. Если вы хотите добавить константы в модуль, сохраните возвращённое значение от Py_InitModule, которое является объектом модуля. Наиболее общий способ добавления элементов в модуль — получение словаря модуля с помощью PyModule_GetDict(module). Используя словарь модуля, вы можете вручную добавить всё, что вам нужно, в модуль. Более простой способ добавления объектов в модуль — использование одного из трёх дополнительных вызовов 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{name}) для:
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) строку документации для функции. Любое количество функций может быть определено для одного модуля путём добавления дополнительных записей в эту таблицу. Последняя запись должна быть полностью NULL, как показано, для того чтобы служить контрольной точкой. Python ищет эту запись, чтобы узнать, что все функции для модуля определены.
Последнее, что нужно сделать для завершения модуля расширения — написать код, выполняющий необходимые функции. Существует два типа функций: те, которые не принимают ключевые аргументы, и те, которые принимают.
Функции без ключевых аргументов
Функции, которые не принимают ключевые аргументы, должны быть написаны как:
static PyObject*
nokeyword_cfunc (PyObject *dummy, PyObject *args)
{
/* convert Python arguments */
/* do function */
/* return something */
}
Аргумент dummy не используется в этом контексте и может быть спокойно проигнорирован. Аргумент args содержит все аргументы, переданные функции в виде кортежа. Вы можете сделать всё, что захотите на этом этапе, но, как правило, самый простой способ управления входными аргументами — вызвать PyArg_ParseTuple (args, format_string, адреса_C_переменных…) или PyArg_UnpackTuple (кортеж, “имя”, min, max, …). Хорошее описание того, как использовать первую функцию, содержится в справочнике Python C-API в разделе 5.5 (Разбор аргументов и построение значений). Следует уделить особое внимание формату “O&”, который использует функции преобразования для перехода между Python-объектом и C-объектом. Все остальные функции форматирования можно (в основном) рассматривать как частные случаи этого общего правила. В NumPy C-API определены несколько функций преобразования, которые могут быть полезны. В частности, функция PyArray_DescrConverter очень полезна для поддержки произвольного указания типа данных. Эта функция преобразует любой допустимый Python-объект типа данных в объект PyArray_Descr *. Помните о передаче адреса C-переменных, которые должны быть заполнены.
В исходном коде NumPy много примеров использования PyArg_ParseTuple. Стандартное использование выглядит так:
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 (format_string, 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, которые в противном случае соответствуют требованиям, будут проходить. Если вы хотите гарантировать массив базового класса, то используйте 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 (obj) для массива с установленными этими флагами. Это сделает базовый массив доступным для записи без копирования содержимого обратно в исходный массив.
Другие полезные флаги, которые можно объединить как дополнительные требования, это:
Преобразование к требуемому типу, даже если это невозможно без потери информации.
Убедитесь, что результирующий массив является копией исходного.
Убедитесь, что результирующий объект является фактическим 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.19/user/c-info.how-to-extend.html