Check if object is iterable in python c extension

Viewed 334

I need to write a function that does different things depending on whether a python argument is iterable or not. Here's a code example:

PyObject *iter = PyObject_GetIter(arg); // arg is a PyObject*
if (iter) {
  // do iterable things
} else {
  // do non-iterable things
}

It appears, though, that if arg is not iterable, then not only is iter == NULL, but an exception is also thrown. What is the right way to handle this situation? Do I just call PyErr_Clear() and hope that no other errors have been set?

1 Answers

In pure Python, the usual solution for finding out whether an object is iterable is to call iter(...) and to see what happens (it was for example popularized by "Fluent Python"):

def is_iterable(obj):
    try:
        iter(obj)  # ok, it worked
        return True
    except TypeError:
        return False

for more details, see this great answer.

This is also basically the @falsetru's proposal in comments - to try and to clear error, if PyObject_GetIter fails:

int is_iterator(PyObject *obj){
    PyObject *it =  PyObject_GetIter(obj);
    if(it != NULL){
        Py_DECREF(it);
        return 1; // object can be iterated
    }
    else if (PyErr_ExceptionMatches(PyExc_TypeError)) {
        PyErr_Clear();
        return 0; // is not an iterator
    }
    else{
        return -1; // error
    }
}

But it might not be what you mean by "being iterable", then one can adjust the implementation of PyObject_GetIter to your needs, for example:

int is_iterator2(PyObject *obj) {
    return Py_TYPE(obj)->tp_iter != NULL || PySequence_Check(obj);
}

As in usual algorithm, is_iterator2 looks up if the tp_iter-slot (i.e. __iter__-function) is present and if not falls back to the sequence protocol via __getitem__. However, differently as in the first version, the tp_iter slot isn't called and its result isn't checked for being an iterator, i.e. for

class C:
    def __iter__(self):
        raise BufferError()
        
class D:
    def __iter__(self):
        return 1; # isn't iterator

C() and D() would be classified as iterable (which would not be the case for the first is_iterator version). Also if is_iterator2 returned 1, it doesn't mean PyObject_GetIter doesn't return NULL as can be seen for the above classes.

Related