Search code examples
cython

How to initialize a tuple from a double array without causing the "Storing unsafe C derivative of temporary" error?


Assuming I know what I am doing, I try to initialize a tuple from a C array of doubles:

from cpython.ref cimport PyObject                                                                                                                                                                   
from cpython.tuple cimport PyTuple_New                                                                                                                                                              
from cpython.float cimport PyFloat_FromDouble                                                                                                                                                       
                                                                                                                                                                                                    
cdef extern from "Python.h":                                                                                                                                                                        
    struct PyTupleObject:                                                                                                                                                                           
        PyObject *ob_item[1]                                                                                                                                                                        
                                                                                                                                                                                                    
# ======================================================================                                                                                                                            
# Tuple utility API                                                                                                                                                                                 
# ======================================================================                                                                                                                            
cdef tuple from_doubles(unsigned n, const double* buffer):                                                                                                                                          
    """ Create a tuple from an array of floats.                                                                                                                                                     
    """                                                                                                                                                                                             
    cdef tuple t = PyTuple_New(n)                                                                                                                                                                   
    cdef PyObject** items = (<PyTupleObject*>t).ob_item                                                                                                                                             
    cdef unsigned i                                                                                                                                                                                 
    for i in range(n):                                                                                                                                                                              
        items[i] = <PyObject*>PyFloat_FromDouble(buffer[i])                                                                                                                                         
                                                                                                                                                                                                    
    return t

From my understanding, PyFloat_FromDouble creates a new object, which is stolen by the tuple when it is stored in items[i]. This is what PyTuple_SET_ITEM does but without the redundant type checking. Still as far as I understand, no Py_INCREF/Py_DECREF calls are needed here.

From the doc:

Few functions steal references; the two notable exceptions are PyList_SetItem() and PyTuple_SetItem(), which steal a reference to the item (but not to the tuple or list into which the item is put!). These functions were designed to steal a reference because of a common idiom for populating a tuple or list with newly created objects; for example, the code to create the tuple (1, 2, "three") could look like this (forgetting about error handling for the moment; a better way to code this is shown below):

PyObject *t;

t = PyTuple_New(3);
PyTuple_SetItem(t, 0, PyLong_FromLong(1L));
PyTuple_SetItem(t, 1, PyLong_FromLong(2L));
PyTuple_SetItem(t, 2, PyUnicode_FromString("three"));

However, when I compile that code with Cython 0.26.1, I get the following errors:

Error compiling Cython file:
------------------------------------------------------------
...
    """
    cdef tuple t = PyTuple_New(n)
    cdef PyObject** items = (<PyTupleObject*>t).ob_item
    cdef unsigned i
    for i in range(n):
        items[i] = <PyObject*>PyFloat_FromDouble(buffer[i])
                  ^
------------------------------------------------------------

fin/tuplex.pyx:19:19: Casting temporary Python object to non-numeric non-Python type

Error compiling Cython file:
------------------------------------------------------------
...
    """
    cdef tuple t = PyTuple_New(n)
    cdef PyObject** items = (<PyTupleObject*>t).ob_item
    cdef unsigned i
    for i in range(n):
        items[i] = <PyObject*>PyFloat_FromDouble(buffer[i])
                  ^
------------------------------------------------------------

fin/tuplex.pyx:19:19: Storing unsafe C derivative of temporary Python reference

How could I fix my code to properly store the newly created PyFloat in the tuple?


Solution

  • I had to provide my own external definition for PyFloat_FromDouble in order to avoid all kinds of automatic reference management from Cython:

    from cpython.ref cimport PyObject
    from cpython.tuple cimport PyTuple_New
    
    cdef extern from "Python.h":
        PyObject* PyFloat_FromDouble(double v)
        # Above:
        # Do NOT use the definition from `cpython.float` to avoid
        # automatic DECREF calls.
    
        ctypedef struct PyTupleObject:
            PyObject *ob_item[1]
    

    By defining PyFloat_FromDouble as returning a PyObject* instead of an object as it is done in cpython.float

    1. I avoid Cython to generate a faulty Py_DECREF call, and
    2. I get rid of the "Storing unsafe C derivative of temporary Python reference" error.

    For reference here is the remaining code:

    # ======================================================================
    # Tuple utility API
    # ======================================================================
    cdef tuple from_doubles(unsigned n, const double* buffer):
        """ Create a tuple from an array of floats.
        """
        cdef tuple t = PyTuple_New(n)
        cdef PyObject** items = (<PyTupleObject*>t).ob_item
        cdef unsigned i
        for i in range(n):
            items[i] = PyFloat_FromDouble(buffer[i])
    
        return t
    
    def test():
        cdef double[1000] values = list(range(1000))
        return from_doubles(1000, values)