Search code examples
python-3.xpython-c-api

Convert an iterable object to tuple in python C api


I have an iterable PyObject that I need to pass as the list of arguments to a Python callable, ala

xs = [1,2,5,7]
some_function(*xs)

However, PyObject_CallObject only allows tuples to be passed as the arguments' container.

I'm not sure, though, what is the best way to create a new tuple containing the elements of an iterable. Here's a tentative code:

PyObject* unpack_call(PyObject* callable, PyObject* args) {
  PyObject* tuple;
  if (PyTuple_Check(args)) {
    tuple = args;
  } else {
    PyObject* iter = PyObject_GetIter(args);
    if (!iter) return NULL;

    tuple = PyTuple_New(0);

    for (Py_ssize_t pos=0; ; ++pos) {
      PyObject* arg = PyIter_Next(iter);
      if (!arg) break;

      PyTuple_SetItem(tuple,pos,arg);
    }
  }
  return PyObject_CallObject(callable,tuple);
}

I'm not sure if I need to grow the size of the tuple myself or not. What's confusing me is the sentence in the documentation saying:

The tuple will always grow or shrink at the end.

I'm also not sure if I need to increment or decrement reference counts for any of these objects.


Solution

  • You're much better using PySequence_Tuple which can create a tuple directly from an iterable. There's probably other similar options to do this too.

    If you did want to do it your way then you do need to call PyTuple_Resize each time you add to it (or resize it in chunks possibly). What the sentence

    The tuple will always grow or shrink at the end.

    tells you is that extra space is at the end of the tuple.

    PyTuple_SetItem steals a reference, so you don't need to touch the reference count of the items you're adding. You should be doing some error checking though - PyIter_Next can raise an exception. You also need to decref iter and the tuple when you're done with them.