itertools.combinationsを無限長イテレータに対応させる

pythonのitertools.combinationsやitertools.permutationには無限長のイテレータを渡す事はできません。内部でイテレータをタプルに変換しようとするのでフリーズしてしまいます。

無限長にも対応する、アルゴリズム自体は難しくはありません。

そこで、python-ml-jpで、なぜitertools.combinationsは無限長に対応していないか質問したところ、

タプルの代わりにリストを利用することで、最初に各イテレータから全要素を取り出すことを
やめられるでしょうが、それだとreallocの回数分パフォーマンスが悪くなります。

実際に、無限長のイテレータに対応したバージョンを作ってみました。

>>> from itertools2 import combinations #無限長に対応したバージョン
>>> from itertools import count, islice
>>> it = combinations(count(), 2)       #無限長のを渡してもフリーズしない
>>> list(islice(it, 30))
[(0, 1), (0, 2), (1, 2), (0, 3), (1, 3), (2, 3), (0, 4), (1, 4), (2, 4), (3, 4),
 (0, 5), (1, 5), (2, 5), (3, 5), (4, 5), (0, 6), (1, 6), (2, 6), (3, 6), (4, 6),
 (5, 6), (0, 7), (1, 7), (2, 7), (3, 7), (4, 7), (5, 7), (6, 7), (0, 8), (1, 8)]

そして、時間測定

from __future__ import division, print_function, unicode_literals

import itertools2
import itertools

import timeit
for mod in ["itertools", "itertools2"]:
    print(mod)
    for n, r in [(10, 2), (100, 2), (1000, 2)]:
        t = timeit.Timer(
            "for x in combinations(xrange({}), {}):pass".format(n, r),
            "from {} import combinations".format(mod)
        )
        print(t.timeit(number=100))
    print()

測定結果

itertools
0.000719923900943
0.0634376715476
6.90153106051

itertools2
0.000939225516092
0.083500556635
9.52643394621

やっぱり無限長に対応した物の方が遅いです。1.5倍ぐらいずつかかっています。itertoolsのような基礎的なモジュールで50%の速度差は致命的です。しかし、単に私のコードが下手だからという可能性はぬぐえません。

無限シーケンスにcombinationsを使う機会は、実際どれくらいあるものなんでしょう?

以下、ソース。初めて作ったC拡張を作りました。参照関係のデバッグは地獄です。

/* itertools2.c */

#include "Python.h"
#include "structmember.h"

/* combinations object ************************************************************/

typedef struct
{
    PyObject_HEAD

    PyObject *it;
    PyObject  *pool;            /* input converted to a tuple */
    PyObject *elem;
    Py_ssize_t *indices;        /* one index per result element */
    Py_ssize_t r;               /* size of result tuple */
    Py_ssize_t i;               /* current axis of indices */
    Py_ssize_t first;
    int stopped;                /* set to 1 when the combinations iterator is exhausted */
}
combinationsobject;

static PyTypeObject combinations_type;

int next_indices(Py_ssize_t r, Py_ssize_t n, Py_ssize_t *indices, Py_ssize_t *i)
{
    if (r == 0 || n == 0 || *i < 0 || n <= *i || r < n)
    {
        return 0;
    }
    while(1)
    {
        indices[*i] += 1;
        if(*i == n - 1 && r <= indices[*i])
        {
            return 0;
        }
        else if (*i < n - 1 && indices[*i + 1] <= indices[*i])
        {
            *i += 1;
        }
        else if (0 < *i)
        {
            *i -= 1;
            indices[*i] = -1;
        }
        else
        {
            return 1;
        }
    }
}

static PyObject *
combinations_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    combinationsobject *co;
    Py_ssize_t r;
    PyObject *iterable = NULL;
    Py_ssize_t *indices = NULL;
    PyObject *pool = NULL;
    static char *kwargs[] = {"iterable", "r", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "On:combinations", kwargs,
                                     &iterable, &r))
    {
        return NULL;
    }

    if (r < 0)
    {
        PyErr_SetString(PyExc_ValueError, "r must be non-negative");
        goto error;
    }

    if(r > 0)
    {
        indices = PyMem_Malloc((r - 1) * sizeof(Py_ssize_t));
        if (indices == NULL)
        {
            PyErr_NoMemory();
            goto error;
        }
    }

    pool = PyList_New(0);
    if(pool == NULL)
    {
        goto error;
    }

    /* create combinationsobject structure */
    co = (combinationsobject *) type->tp_alloc(type, 0);
    if (co == NULL)
    {
        goto error;
    }

    co->pool = pool;
    co->indices = indices;
    co->it = PyObject_GetIter(iterable);
    co->first = 1;
    co->r = r;
    co->stopped = 0;

    return (PyObject *)co;
error:
    if (indices != NULL)
    {
        PyMem_Free(indices);
    }
    Py_XDECREF(pool);
    return NULL;
}

static void
combinations_dealloc(combinationsobject *co)
{
    PyObject_GC_UnTrack(co);
    Py_XDECREF(co->pool);
    Py_XDECREF(co->it);
    Py_XDECREF(co->elem);

    if (co->indices != NULL)
    {
        PyMem_Free(co->indices);
    }
    Py_TYPE(co)->tp_free(co);
}

static int
combinations_traverse(combinationsobject *co, visitproc visit, void *arg)
{
    Py_VISIT(co->it);
    Py_VISIT(co->pool);
    Py_VISIT(co->elem);
    return 0;
}

static PyObject *
combinations_next(combinationsobject *co)
{
    Py_ssize_t j;
    PyObject *result;
    Py_ssize_t *indices = co->indices;
    Py_ssize_t *i = &co->i;
    PyObject *pool = co->pool;
    PyObject *it = co->it;
    Py_ssize_t r = co->r;

    if (co->stopped)
    {
        return NULL;
    }
    else if (r == 0)
    {
        co->first = 0;
        co->stopped = 1;
        result = PyTuple_New(0);
        if (result == NULL)
        {
            goto empty;
        }
        return result;
    }
    else if(r == 1)
    {
        PyObject *x = NULL;
        co->first = 0;
        x = PyIter_Next(it);
        if(x == NULL)
        {
            goto empty;
        }
        result = PyTuple_New(1);
        if (result == NULL)
        {
            goto empty;
        }
        PyTuple_SetItem(result, 0, x);
        return result;
    }
    else
    {
        if (co->first)
        {
            PyObject *x = NULL;
            co->first = 0;
            for(j=0; j < r - 1; ++j)
            {
                x = PyIter_Next(it);
                if(x == NULL)
                {
                    goto empty;
                }
                if (PyList_Append(pool, x) == -1)
                {
                    goto empty;
                }
                Py_DECREF(x);
            }
            x = PyIter_Next(it);
            if(x == NULL)
            {
                goto empty;
            }
            co->elem = x;
            for (j=0 ; j < r - 1  ; j++)
            {
                indices[j] = -1;
            }
            *i = r - 2;
        }
        while(1)
        {
            if(next_indices(PyList_Size(pool), r - 1, indices, i))
            {
                PyObject *x;
                result = PyTuple_New(r);
                if (result == NULL)
                {
                    goto empty;
                }
                for(j=0; j < r - 1; ++j)
                {
                    x = PyList_GetItem(pool, indices[j]);
                    if(x == NULL)
                    {
                        goto empty;
                    }
                    Py_INCREF(x);
                    PyTuple_SetItem(result, j, x);
                }
                Py_INCREF(co->elem);
                PyTuple_SetItem(result, r - 1, co->elem);
                return result;
            }
            else
            {
                if (PyList_Append(pool, co->elem) == -1)
                {
                    goto empty;
                }
                Py_DECREF(co->elem);
                co->elem = PyIter_Next(it);
                if(co->elem == NULL)
                {
                    goto empty;
                }
                for (j=0 ; j < r - 1  ; j++)
                {
                    indices[j] = -1;
                }
                *i = r - 2;
            }
        }
    }
empty:
    //Py_XDECREF(result);
    co->stopped = 1;
    return NULL;
}

PyDoc_STRVAR(combinations_doc,
             "combinations(iterable, r) --> combinations object\n\
             \n\
             Return successive r-length combinations of elements in the iterable.\n\n\
             combinations(range(4), 3) --> (0,1,2), (0,1,3), (0,2,3), (1,2,3)");

static PyTypeObject combinations_type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "itertools2.combinations",                   /* tp_name */
    sizeof(combinationsobject),         /* tp_basicsize */
    0,                                  /* tp_itemsize */
    /* methods */
    (destructor)combinations_dealloc,           /* tp_dealloc */
    0,                                  /* tp_print */
    0,                                  /* tp_getattr */
    0,                                  /* tp_setattr */
    0,                                  /* tp_compare */
    0,                                  /* tp_repr */
    0,                                  /* tp_as_number */
    0,                                  /* tp_as_sequence */
    0,                                  /* tp_as_mapping */
    0,                                  /* tp_hash */
    0,                                  /* tp_call */
    0,                                  /* tp_str */
    PyObject_GenericGetAttr,            /* tp_getattro */
    0,                                  /* tp_setattro */
    0,                                  /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
    Py_TPFLAGS_BASETYPE,            /* tp_flags */
    combinations_doc,                           /* tp_doc */
    (traverseproc)combinations_traverse,        /* tp_traverse */
    0,                                  /* tp_clear */
    0,                                  /* tp_richcompare */
    0,                                  /* tp_weaklistoffset */
    PyObject_SelfIter,                  /* tp_iter */
    (iternextfunc)combinations_next,            /* tp_iternext */
    0,                                  /* tp_methods */
    0,                                  /* tp_members */
    0,                                  /* tp_getset */
    0,                                  /* tp_base */
    0,                                  /* tp_dict */
    0,                                  /* tp_descr_get */
    0,                                  /* tp_descr_set */
    0,                                  /* tp_dictoffset */
    0,                                  /* tp_init */
    0,                                  /* tp_alloc */
    combinations_new,                           /* tp_new */
    PyObject_GC_Del,                    /* tp_free */
};



/* module level code ********************************************************/

PyDoc_STRVAR(module_doc,
             "");


static PyMethodDef module_methods[] = {
  {NULL, NULL}           /* sentinel */
};

PyMODINIT_FUNC
inititertools2(void)
{
    int i;
    PyObject *m;
    char *name;
    PyTypeObject *typelist[] = {
       &combinations_type,
       NULL
   };

    m = Py_InitModule3("itertools2", module_methods, module_doc);
    if (m == NULL)
        return;

    for (i=0 ; typelist[i] != NULL ; i++)
    {
        if (PyType_Ready(typelist[i]) < 0)
            return;
        name = strchr(typelist[i]->tp_name, '.');
        assert (name != NULL);
        Py_INCREF(typelist[i]);
        PyModule_AddObject(m, name+1, (PyObject *)typelist[i]);
    }
}