Repository: numpy/numpy
File: numpy/_core/src/umath/ufunc_object.c (C)

Code before:
{
    return PyLong_FromLong(ufunc->ntypes);
}

static PyObject *
ufunc_get_types(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored))
{
    /* return a list with types grouped input->output */
    PyObject *list;
    PyObject *str;
    int k, j, n, nt = ufunc->ntypes;
    int ni = ufunc->nin;
    int no = ufunc->nout;
    char *t;
    list = PyList_New(nt);
    if (list == NULL) {
        return NULL;
    }
    t = PyArray_malloc(no+ni+2);
    n = 0;
    for (k = 0; k < nt; k++) {
        for (j = 0; j<ni; j++) {
            t[j] = _typecharfromnum(ufunc->types[n]);
            n++;
        }
        t[ni] = '-';
        t[ni+1] = '>';
        for (j = 0; j < no; j++) {
            t[ni + 2 + j] = _typecharfromnum(ufunc->types[n]);
            n++;
        }
        str = PyUnicode_FromStringAndSize(t, no + ni + 2);
        PyList_SET_ITEM(list, k, str);
    }
    PyArray_free(t);
    return list;
}

static PyObject *
ufunc_get_name(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored))
{
    return PyUnicode_FromString(ufunc->name);
}

static PyObject *
ufunc_get_identity(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored))
{
    npy_bool reorderable;
    return PyUFunc_GetDefaultIdentity(ufunc, &reorderable);
}


Review comment (suggestion):
(Sorry, I had noticed this yesterday, and should have commented right away.)

How bad is it if we don't do this dance?  A custom getattr feels like a bit of a code smell, and disables some optimizations I am pretty sure (e.g. `np.add.reduce(...)` will now have to actually _create_ `np.add.reduce` (the bound method) and call that rather than calling `np.add.reduce` directly.

In Python, it is simple to hack around it. Unfortunately, in C it seems harder unfortunately.
(I think we might have to make `ufunc` a heaptype so that it has a `__dict__` itself and we can use a custom descriptor just like we could in Python.)

So I guess a comment for now, although since converting to the heaptype is probably the trickiest part of this, maybe it is actually not that bad.

Code after:

    signature = PyObject_CallFunctionObjArgs(
            npy_runtime_imports._ufunc_inspect_signature_builder,
            (PyObject *)ufunc, NULL);
    if (signature == NULL) {
        return NULL;
    }

    // Cache the result in the instance dict for next time
    if (PyDict_SetItem(ufunc->dict, npy_interned_str.__signature__,
                       signature) < 0) {
        Py_DECREF(signature);
        return NULL;
    }

    return signature;
}

static PyObject *
ufunc_getattro(PyObject *obj, PyObject *name)
{
    // __signature__ special-casing to prevent class attribute access
    if (PyUnicode_Check(name) &&
        PyUnicode_Compare(name, npy_interned_str.__signature__) == 0) {
        return ufunc_get_inspect_signature((PyUFuncObject *)obj, NULL);
    }
    
    // For all other attributes, use default behavior
    return PyObject_GenericGetAttr(obj, name);
}

static PyObject *
ufunc_get_nin(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored))
{
    return PyLong_FromLong(ufunc->nin);
}

static PyObject *
ufunc_get_nout(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored))
{
    return PyLong_FromLong(ufunc->nout);
}

static PyObject *
ufunc_get_nargs(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored))
{
    return PyLong_FromLong(ufunc->nargs);
}

static PyObject *
ufunc_get_ntypes(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored))