Benjamin Dobell
Benjamin Dobell

Reputation: 4072

Inheriting from a tuple subclass

I'm using a third-party module which provides classes that inherit from tuple. However, I'd like to add some functionality to these classes, so I have sub-classed. The resultant inheritance hierarchy looks like:

MyClass -> LibClass -> tuple

Is there any reason inheritance from a tuple sub-class should be expected to fail?

The gory details

Everything seems to be peachy at first. However, utilising a slice (instance[:6]) to access a range of values from an instance of MyClass results in an error like:

SystemError: <method-wrapper '__getitem__' of LibClass object at 0x1010101010> returned NULL without setting an error

Doing the exact same thing with an instance of LibClass works flawlessly.

To further add to the mystery, regular indexed access (instance[5]) on an instance of MyClass works flawlessly.

Obviously tuple inheritance isn't exactly like regular class inheritance (i.e. __new__ must be overridden rather than __init__). However, to my knowledge LibClass is doing so correctly e.g.

def __new__(cls, *members):
    mat = [x * 1.0 for x in members] + [0.0, 0.0, 1.0]
    return tuple.__new__(cls, mat)

I don't believe implementing __new__ in MyClass is necessary given the implementation in LibClass correctly passes through cls (admittedly I had to fork the library to achieve that). Nonetheless, for posterity I did also try implementing __new__ directly in MyClass (just copied and pasted the LibClass implementation).

I should also note that I'm not doing anything wacky in MyClass. In fact if I do nothing at all the problem still persists e.g.

class MyClass(lib.LibClass):
    pass

Another thing worth noting, LibClass does not have a custom __getitem__ implementation - it's simply inheriting that behaviour from tuple.

Python 3.6.1

The extra gory (real) details

LibClass is in fact Affine from planar, my fork can be found here:

https://github.com/Benjamin-Dobell/planar/blob/master/lib/planar/transform.py

Reproduction

pip install git+https://github.com/Benjamin-Dobell/planar#egg=planar
python

>>> import planar
>>> class Affine(planar.Affine):
...     pass
... 
>>> planar.Affine.identity()[:6]
(1.0, 0.0, 0.0, 0.0, 1.0, 0.0)
>>> Affine.identity()[:6]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
SystemError: <method-wrapper '__getitem__' of Affine object at 0x10e2b9ba8> returned NULL without setting an error

It was pointed out in the comments that in the above reproduction identity() returns a constant. So it really shouldn't be failing. I can't explain that. However, I should probably add that it was a pretty poor reproduction by me. My real world usage is closer to:

>>> Affine.translation((0, 0))[:6]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
SystemError: <method-wrapper '__getitem__' of Affine object at 0x10f8d5ee8> returned NULL without setting an error
>>> planar.Affine.translation((0, 0))[:6]
(1.0, 0.0, 0.0, 0.0, 1.0, 0.0)

which also fails in the same fashion.

Mind you, the failure with the constant really has me scratching my head.

Something planar specific

Trying different Python versions, it fails similarly:

3.3.6

Python 3.3.6 (default, Apr 12 2017, 17:20:32) 
[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.38)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import planar
>>> class Affine(planar.Affine):
...     pass
... 
>>> planar.Affine.identity()[:6]
(1.0, 0.0, 0.0, 0.0, 1.0, 0.0)
>>> Affine.identity()[:6]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
SystemError: NULL result without error in PyObject_Call

2.7.11

Python 2.7.11 (default, May  2 2016, 14:38:51) 
[GCC 4.2.1 Compatible Apple LLVM 7.3.0 (clang-703.0.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import planar       
>>> class Affine(planar.Affine):
...     pass
... 
>>> planar.Affine.identity()[:6]
(1.0, 0.0, 0.0, 0.0, 1.0, 0.0)
>>> Affine.identity()[:6]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
SystemError: NULL result without error in PyObject_Call

However, I was not able to reproduce the problem when reduced to its simplest form i.e. (Python 2.7.11):

>>> class LibClass(tuple):
...     def __new__(cls, *members):
...         return tuple.__new__(cls, *members)
... 
>>> class MyClass(LibClass):
...     pass
... 
>>> LibClass((1, 2, 3, 4, 5))[:3]
(1, 2, 3)
>>> MyClass((1, 2, 3, 4, 5))[:3]
(1, 2, 3)

I also tried moving the definition of LibClass to a separate lib.py to ensure the error wasn't somehow related to Python modules, but it worked as above.

So the issue is something specific to planar and/or its Affine class. It'd be nice to know exactly what is causing the problem though.

Upvotes: 2

Views: 435

Answers (1)

user2357112
user2357112

Reputation: 280564

It turns out there is indeed a buggy extension module involved. planar isn't using your edited planar.transform module at all; it's using planar.c, a C implementation of planar's functionality with its own Affine class.

At least part of the problem seems to be due to a bug in Affine_getitem:

static PyObject *
Affine_getitem(PlanarAffineObject *self, Py_ssize_t i)
{
    double m;

    assert(PlanarAffine_Check(self));
    if (i < 6) {
        m = self->m[i];
    } else if (i < 8) {
        m = 0.0;
    } else if (i == 8) {
        m = 1.0;
    } else {
        return NULL;
    }
    return PyFloat_FromDouble(m);
}

where it returns NULL without setting an IndexError for an out-of-range index.

planar is no longer maintained, so a bug report won't be handled. There may be better modules to use.

Upvotes: 1

Related Questions