NumPy C-API: プログラミングから UFUNC_SHIFT_DIVIDEBYZERO を活用する方法


フラグの意味

  • UFUNC_SHIFT_DIVIDEBYZERO が 2 の場合
    ゼロで割る操作は inf (正または負の無限大) で置き換えられます。
  • UFUNC_SHIFT_DIVIDEBYZERO が 1 の場合
    ゼロで割る操作は NaN (Not a Number) で置き換えられます。
  • UFUNC_SHIFT_DIVIDEBYZERO が 0 の場合
    ゼロで割る操作はエラーとして扱われ、例外が送出されます。

使用例

#include <numpy/ufunc.h>

void my_ufunc(int *x, int *y, int *z, Py_ssize_t *dimensions, Py_ssize_t *strides) {
  // ...

  // ゼロで割る可能性がある演算を実行

  if (y[0] == 0) {
    if (ufunc->options->ufunc_shift_dividebyzero == 0) {
      // エラーとして処理
      PyErr_SetString(PyExc_ZeroDivisionError, "division by zero");
      return;
    } else if (ufunc->options->ufunc_shift_dividebyzero == 1) {
      // NaN で置き換え
      z[0] = NPY_NAN;
    } else if (ufunc->options->ufunc_shift_dividebyzero == 2) {
      // 無限大で置き換え
      z[0] = NPY_INFINITY;
    }
  } else {
    // ...
  }
}

// ...

PyUFuncObject *ufunc = PyUFunc_FromFuncAndDescr(my_ufunc, NULL, "iiii", 0,
                                             PY_INT, PY_INT, PY_INT, 0);

// ...

// `UFUNC_SHIFT_DIVIDEBYZERO` フラグを設定
ufunc->options->ufunc_shift_dividebyzero = 1;

// ...
  • UFUNC_SHIFT_DIVIDEBYZERO フラグを変更すると、既存の ufunc オブジェクトのパフォーマンスに影響を与える可能性があります。
  • UFUNC_SHIFT_DIVIDEBYZERO フラグを変更する前に、PyUFunc_CheckOpcode 関数を使用して、ufunc オブジェクトがゼロで割る操作をサポートしていることを確認する必要があります。


#include <numpy/ufunc.h>

static PyObject *
my_ufunc(PyObject *self, PyObject *args) {
  PyUFuncObject *ufunc = (PyUFuncObject *)self;
  PyArrayObject *x = (PyArrayObject *)PyArray_FromAny(args[0], NULL, 0, 0, NPY_ARRAY_CONTIGUOUS | NPY_ARRAY_ALIGNED, NULL);
  PyArrayObject *y = (PyArrayObject *)PyArray_FromAny(args[1], NULL, 0, 0, NPY_ARRAY_CONTIGUOUS | NPY_ARRAY_ALIGNED, NULL);
  PyArrayObject *z = (PyArrayObject *)PyArray_NewFromDescr(&PyArray_Descr(NPY_INT), PyArray_DimView(x), NULL, NULL);

  if (x == NULL || y == NULL || z == NULL) {
    Py_XDECREF(x);
    Py_XDECREF(y);
    Py_XDECREF(z);
    return NULL;
  }

  if (PyUFunc_Apply(ufunc, 2, 2, (PyArrayObject **)&x, (PyArrayObject **)&y, (PyArrayObject **)&z, NULL, NULL, 0) < 0) {
    Py_XDECREF(x);
    Py_XDECREF(y);
    Py_XDECREF(z);
    return NULL;
  }

  Py_XDECREF(x);
  Py_XDECREF(y);
  return PyArray_Return(z);
}

static PyMethodDef methods[] = {
  {"my_ufunc", (PyCFunction)my_ufunc, METH_VARARGS, "My UFunc"},
  {NULL, NULL, 0, NULL}
};

PyModuleDef moduledef = {
  PyModule_DEF_HEAD("my_module", NULL, NULL, NULL, 0),
  methods,
};

PyMODINIT_FUNC(my_module, moduledef) {
  PyObject *m;

  m = PyModule_Create(&moduledef);
  if (m == NULL) return NULL;

  if (PyImport_AddModule(my_module) < 0) {
    Py_DECREF(m);
    return NULL;
  }

  return m;
}

int main(int argc, char *argv[]) {
  Py_Initialize();

  if (PyImport_ImportModule("numpy") == NULL) {
    PyErr_PrintEx(0);
    Py_Finalize();
    return 1;
  }

  PyModuleDef *moduledef = &moduledef;
  PyObject *m = PyModule_Create(moduledef);
  if (m == NULL) {
    Py_Finalize();
    return 1;
  }

  // `UFUNC_SHIFT_DIVIDEBYZERO` フラグを設定
  PyUFuncObject *ufunc = PyUFunc_FromFuncAndDescr(my_ufunc, NULL, "iiii", 0,
                                             PY_INT, PY_INT, PY_INT, 0);
  if (ufunc == NULL) {
    Py_DECREF(m);
    Py_Finalize();
    return 1;
  }

  ufunc->options->ufunc_shift_dividebyzero = 1;

  if (PyImport_AddModule(my_module) < 0) {
    Py_DECREF(m);
    Py_Finalize();
    return 1;
  }

  Py_Finalize();
  return 0;
}

このコードは以下の機能を提供します。

  • my_module モジュール: my_ufunc 関数を公開します。
  • my_ufunc 関数: ufunc オブジェクトを受け取り、ゼロで割る操作を NaN で置き換えます。
  1. 上記のコードを保存して、my_module.c という名前のファイルにします。
  2. 以下のコマンドを実行して、モジュールをコンパイルします。
gcc -o my_module.so -shared -I/usr/include/python3.10 -I/usr/lib/python3.10/site-packages


try-except ブロック

ゼロで割る可能性があるコードを try-except ブロックで囲むことで、エラーを捕捉して適切な処理を行うことができます。

def my_function(x, y):
  try:
    result = x / y
  except ZeroDivisionError:
    result = np.nan
  return result

np.where 関数

np.where 関数を使用して、ゼロで割る可能性がある箇所を条件式で置き換えることができます。

def my_function(x, y):
  result = np.where(y != 0, x / y, np.nan)
  return result

カスタム ufunc

ゼロで割る操作を NaN で置き換えるように、独自の ufunc を作成することができます。

import numpy as np

def my_divide(x, y):
  result = np.empty_like(x)
  np.divide(x, y, out=result, where=(y != 0))
  result[y == 0] = np.nan
  return result

ufunc = np.frompyfunc(my_divide, 2, 1)

result = ufunc(x, y)

キャッチングと再スロー

ゼロで割るエラーをキャッチして、別のエラーとして再スローすることができます。

def my_function(x, y):
  try:
    result = x / y
  except ZeroDivisionError:
    raise MyCustomError("Division by zero")
  return result

NumPy 以外にも、ゼロで割る操作を安全に処理できるライブラリがいくつかあります。 例えば、pandas ライブラリには fillna メソッドがあり、欠損値を NaN で置き換えることができます。

import pandas as pd

series = pd.Series([1, 2, 0, 4, 5])
result = series / series.iloc[2]
print(result.fillna(np.nan))
  • ゼロで割る可能性があるコードを扱う際には、常に注意が必要です。
  • UFUNC_SHIFT_DIVIDEBYZERO フラグを使用する場合は、既存の ufunc オブジェクトのパフォーマンスに影響を与える可能性があることを覚えておく必要があります。