I'm building cython extended types, and I've always been bothered that I had to make class attributes public for other extended types to be able to see them. But now than I'm also making subclasses I've even more surprised.
The following code
@cython.cclass
class Base:
base_attrib = cython.declare(cython.double, 0.0)
@cython.cclass
class Derived(Base):
derived_attrib = cython.declare(cython.double, 0.0)
@cython.cfunc
def f_on_derived(a: Derived, b: Derived) -> Derived:
res = Derived.__new__(Derived)
ad = a.derived_attrib
bd = b.derived_attrib
ab = a.base_attrib
bb = b.base_attrib
res.derived_attrib = ad + bd
res.base_attrib = ab + bb
return res
Produces a .c file, but the compiler then complains
src/crujisim/cythontests.c(40975): error C2065: 'base_attrib': undeclared identifier
src/crujisim/cythontests.c(40975): warning C4244: '=': conversion from 'double' to 'int', possible loss of data
src/crujisim/cythontests.c(41007): error C2065: 'derived_attrib': undeclared identifier
src/crujisim/cythontests.c(41007): warning C4244: '=': conversion from 'double' to 'int', possible loss of data
As it is a C function I would have expected the typing annotation to be enough, but it isn't.
I can make it compile by declaring public visibility, like
@cython.cclass
class Base:
base_attrib = cython.declare(cython.double, visibility='public')
@cython.cclass
class Derived(Base):
derived_attrib = cython.declare(cython.double, visibility='public')
But then the C code for res.base_attrib = ab + bb does have to go through python, like
__pyx_t_1 = PyFloat_FromDouble((__pyx_v_ab + __pyx_v_bb))
if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 26, __pyx_L1_error)__Pyx_GOTREF(__pyx_t_1)
if (__Pyx_PyObject_SetAttrStr(__pyx_v_res, __pyx_n_s_base_attrib, __pyx_t_1) < 0) __PYX_ERR(0, 26, __pyx_L1_error)__Pyx_DECREF(__pyx_t_1)
__pyx_t_1 = 0;
So two questions:
Can I have superclass attributes that are C only but accessible to subclasses?
Can I have class attributes that are C only and yet visible to C code in other instances?
Update
I've just noticed that if don't use fast instantiation that is res = Derived()
instead of res = Derived.__new__(Derived)
attributes do work as expected. But of course I've also now lost the fast instantiation.
Can I have my cake and eat it too?
So several issues where at play here:
The compiler errors were due to the attribute declaration including a value. Leaving them just like base_attrib = cython.declare(cython.double)
removed the warning and the values where initialized to 0 automatically all the same.
The other issue was that the object produced through fast instantiation had to through python to access its attributes, whereas the python instantation didn't. This was because the __new__
method produces a python object, not the C version. So in the code the only problem was accessing the attributes of the new instance, not the ones passed as arguments.
This was solved with declaring the variable holding the object returned by fast instantiation.
So the working fastest version of the original problem yet is
@cython.cclass
class Base:
base_attrib = cython.declare(cython.double)
@cython.cclass
class Derived(Base):
derived_attrib = cython.declare(cython.double)
@cython.cfunc
def f_on_derived(a: Derived, b: Derived) -> Derived:
res: Derived = Derived.__new__(Derived) # Notice res: Derived
ad = a.derived_attrib
bd = b.derived_attrib
ab = a.base_attrib
bb = b.base_attrib
res.derived_attrib = ad + bd
res.base_attrib = ab + bb
return res
In order to test speed I have these two functions
@cython.ccall
def python_instantiate() -> Derived:
o = Derived()
return o
@cython.ccall
def cython_fast_instantiate() -> Derived:
o: Derived = Derived.__new__(Derived)
return o
and timing them we get
In [2]: %timeit python_instantiate()
87.5 ns ± 0.895 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
In [3]: %timeit cython_fast_instantiate()
62.1 ns ± 0.574 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
Which proves that fast instantiation is faster, even though there is some python object reference incrementing and decrementing that I'm not sure is completely necessary.