NumPy C-API: type PyUFuncObjectでUFuncを作成する方法


type PyUFuncObject の構造

type PyUFuncObject は、以下のメンバを持つ構造体です。

  • module: UFunc が定義されているモジュール
  • doc: UFunc のドキュメント文字列
  • fname: UFunc の名前
  • dispatcher: UFunc のディスパッチテーブル
  • reducers: 削減操作用の関数ポインタの配列
  • check_dtype: 入力と出力のデータ型をチェックする関数ポインタ
  • flags: UFunc の動作を制御するフラグの集合
  • userdata: ユーザーデータポインタ
  • nout: 出力配列の数を指定する整数
  • nin: 入力配列の数を指定する整数
  • ntype: 入力と出力のデータ型を指定する NumPy 型オブジェクトの配列
  • PyObject_HEAD: Python オブジェクトの標準的なヘッダー

type PyUFuncObject を操作するには、以下の関数を使用できます。

  • PyUFunc_Register: UFunc オブジェクトを NumPy に登録します。
  • PyUFunc_FromFunc: Python 関数から UFunc オブジェクトを作成します。
  • PyUFunc_GetInfo: UFunc オブジェクトに関する情報を取得します。
  • PyUFunc_Check: オブジェクトが type PyUFuncObject であるかどうかを確認します。
  • PyUFunc_New: 新しい UFunc オブジェクトを作成します。

以下の例では、type PyUFuncObject を使用して、2 つの配列の合計を計算する UFunc を作成する方法を示します。

#include <Python.h>
#include <numpy/ndarray.h>

static PyObject *
add_ufunc(PyObject *self, PyObject *args) {
  PyArrayObject *arr1, *arr2;

  if (!PyArg_ParseTuple(args, "OO", &arr1, &arr2)) {
    return NULL;
  }

  if (PyArray_NDIM(arr1) != PyArray_NDIM(arr2)) {
    PyErr_SetString(PyExc_ValueError, "Arrays must have the same number of dimensions");
    return NULL;
  }

  for (int i = 0; i < PyArray_NDIM(arr1); i++) {
    if (PyArray_DIM(arr1, i) != PyArray_DIM(arr2, i)) {
      PyErr_SetString(PyExc_ValueError, "Arrays must have the same dimensions");
      return NULL;
    }
  }

  PyArrayObject *result = (PyArrayObject *)PyArray_Alloc(arr1->descr, PyArray_DIMS(arr1), 0, NULL);
  if (!result) {
    return NULL;
  }

  for (npy_intp *iter = PyArray_IterAll(arr1), *iter2 = PyArray_IterAll(arr2); *iter; ++iter, ++iter2) {
    ((char *)result->data)[*iter] = ((char *)arr1->data)[*iter] + ((char *)arr2->data)[*iter2];
  }

  PyArray_IterNext(arr1);
  PyArray_IterNext(arr2);

  return (PyObject *)result;
}

static PyMethodDef add_ufunc_methods[] = {
  {"add", add_ufunc, METH_VARARGS, "Add two arrays"},
  {NULL, NULL, 0, NULL}
};

static PyModuleDef add_ufunc_module = {
  PyModule_DEF_HEAD("add_ufunc", NULL, NULL, NULL, 1),
  add_ufunc_methods,
};

PyMODINIT_FUNC PyInit_add_ufunc(void) {
  return PyModule_Create(&add_ufunc_module);
}


#include <Python.h>
#include <numpy/ndarray.h>

static PyObject *
add_ufunc(PyObject *self, PyObject *args) {
  PyArrayObject *arr1, *arr2;

  if (!PyArg_ParseTuple(args, "OO", &arr1, &arr2)) {
    return NULL;
  }

  if (PyArray_NDIM(arr1) != PyArray_NDIM(arr2)) {
    PyErr_SetString(PyExc_ValueError, "Arrays must have the same number of dimensions");
    return NULL;
  }

  for (int i = 0; i < PyArray_NDIM(arr1); i++) {
    if (PyArray_DIM(arr1, i) != PyArray_DIM(arr2, i)) {
      PyErr_SetString(PyExc_ValueError, "Arrays must have the same dimensions");
      return NULL;
    }
  }

  PyArrayObject *result = (PyArrayObject *)PyArray_Alloc(arr1->descr, PyArray_DIMS(arr1), 0, NULL);
  if (!result) {
    return NULL;
  }

  for (npy_intp *iter = PyArray_IterAll(arr1), *iter2 = PyArray_IterAll(arr2); *iter; ++iter, ++iter2) {
    ((char *)result->data)[*iter] = ((char *)arr1->data)[*iter] + ((char *)arr2->data)[*iter2];
  }

  PyArray_IterNext(arr1);
  PyArray_IterNext(arr2);

  return (PyObject *)result;
}

static PyMethodDef add_ufunc_methods[] = {
  {"add", add_ufunc, METH_VARARGS, "Add two arrays"},
  {NULL, NULL, 0, NULL}
};

static PyModuleDef add_ufunc_module = {
  PyModule_DEF_HEAD("add_ufunc", NULL, NULL, NULL, 1),
  add_ufunc_methods,
};

PyMODINIT_FUNC PyInit_add_ufunc(void) {
  return PyModule_Create(&add_ufunc_module);
}

このコードは以下の処理を実行します。

  1. add_ufunc 関数を作成します。この関数は、2 つの NumPy 配列を受け取り、それらの合計を返す UFunc です。
  2. PyArg_ParseTuple 関数を使用して、add_ufunc 関数に渡される引数を解析します。この関数は、2 つの配列が渡されていることを確認し、それらが同じ次元数であることを確認します。
  3. PyArray_Alloc 関数を使用して、結果を格納するための新しい NumPy 配列を作成します。
  4. PyArray_IterAll 関数を使用して、入力配列のすべての要素を反復処理します。
  5. 各要素について、入力配列の対応する要素の値を合計し、結果配列の対応する要素に格納します。
  6. PyArray_IterNext 関数を使用して、入力配列のイテレータを次の要素に進めます。
  7. 結果配列を返します。


NumPy のコア関数を使用する

NumPy には、addsubtractmultiply などの一般的な演算を実行するためのコア関数セットが用意されています。これらの関数は、type PyUFuncObject を使用せずに UFunc を実装する簡単な方法を提供します。

import numpy as np

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

result = np.add(arr1, arr2)
print(result)  # [5 7 9]

NumPy の vectorize 関数を使用する

vectorize 関数は、Python 関数を NumPy UFunc に変換するために使用できます。これは、type PyUFuncObject を手動で作成する必要がないため、より簡単な方法です。

import numpy as np

def add(a, b):
  return a + b

add_ufunc = np.vectorize(add)

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

result = add_ufunc(arr1, arr2)
print(result)  # [5 7 9]

Cython を使用する

Cython は、C と Python のコードを組み合わせたハイブリッド言語です。Cython を使用すると、type PyUFuncObject を直接操作せずに、NumPy UFunc をより効率的に実装できます。

import numpy as np

def add(a, b):
  cdef double result:
    result = a + b
  return result

add_ufunc = np.frompyfunc(add, 2, 1)

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

result = add_ufunc(arr1, arr2)
print(result)  # [5 7 9]

Numba を使用する

Numba は、Python コードを機械語にコンパイルするためのコンパイラです。Numba を使用すると、type PyUFuncObject を直接操作せずに、NumPy UFunc をより高速に実装できます。

import numpy as np
from numba import jit

@jit
def add(a, b):
  result = np.empty_like(a)
  for i in range(len(a)):
    result[i] = a[i] + b[i]
  return result

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

result = add(arr1, arr2)
print(result)  # [5 7 9]

type PyUFuncObject は、NumPy C-API で最も汎用的な UFunc を実装するための強力なツールですが、より簡単な代替方法もいくつかあります。上記で紹介した方法は、それぞれ異なる長所と短所を持っています。

  • Numba: 最も高速な UFunc を実装したい場合
  • Cython: より効率的な UFunc を実装したい場合
  • NumPy の vectorize 関数: 柔軟性と簡潔さのバランス
  • NumPy のコア関数: 最も簡単で、多くの場合で十分な機能を提供します。