Search code examples
pythontypesgeneratorcythonisinstance

How to test if Cython property is generator?


In IPython, I can see that a property of a Cython class is a generator by simply defining it and then calling:

%%cython
cdef class SomeCls:
    property x:
        def __get__(self):
            yield 1

The call looks like

SomeCls().x
# prints <generator at 0x102f61ee8>

I am having trouble testing if that property is a generator:

import types
print(isinstance(SomeCls().x, types.GeneratorType))
# prints False

import inspect
print(inspect.isgeneratorfunction(SomeCls.x))
# prints False

How can I determine whether a property of a Cython class is a generator?


Solution

  • Why doesn't the usual way work?

    First, as you already probably know, there is no difference between inspect.isgeneratorfunction(...) and isinstance(..., types.GeneratorType) - the inspect-module just calls isinstance(..., types.GeneratorType).

    On the other hand, types.GeneratorType is defined as

    def _g():
        yield 1
    GeneratorType = type(_g())
    

    CPython uses PyGenObject (here code, here documentation) for generators, there is no fancy logic for the comparison as for some ABC-classes, so the isinstance will boil down to comparing the C-object types.

    However, Cython returns a __pyx_CoroutineObject for generators (just check the cythonized code to see)

    typedef PyObject *(*__pyx_coroutine_body_t)(PyObject *, PyThreadState *, PyObject *);
    typedef struct {
        PyObject_HEAD
        __pyx_coroutine_body_t body;
        PyObject *closure;
        ...
        int resume_label;
        char is_running;
    } __pyx_CoroutineObject;
    

    which has nothing to do with PyGenObject as far as isinstanceis concerned - it doesn't really care whether generator is in the name of the type (but for us humans it can be really puzzling, because type(obj) says "generator").

    So you will have to roll out your own version of isgenerator, which takes also Cython-"generators" into account. There are many ways, for example

    %%cython
    def _f():
        yield 1
    CyGeneratorType = type(_f())   
    def iscygenerator(o):
        return isinstance(o, CyGeneratorType)
    

    and now:

    import inspect   
    def isgenerator(o):
        return inspect.isgenerator(o)  or iscygenerator(o)
    
    isgenerator(SomeCls().x)          #  True
    iscygenerator(SomeCls().x)        #  True
    inspect.isgenerator(SomeCls().x)  #  False