7

How can we make a class represent itself as a slice when appropriate?

This didn't work:

class MyThing(object):
    def __init__(self, start, stop, otherstuff):
        self.start = start
        self.stop = stop
        self.otherstuff = otherstuff
    def __index__(self):
        return slice(self.start, self.stop)

Expected output:

>>> thing = MyThing(1, 3, 'potato')
>>> 'hello world'[thing]
'el'

Actual output:

TypeError: __index__ returned non-(int,long) (type slice)

Inheriting from slice doesn't work either.

wim
  • 338,267
  • 99
  • 616
  • 750
  • well, from the looks of it seems like `__index__()` stresses on returning a single `int` instead of a `slice`, and it does work fine when returning single integer. – lycuid Oct 11 '16 at 06:07
  • Why do you want to do this, by the way? – MisterMiyagi Oct 11 '16 at 06:17
  • 3
    I am making a kind of rich slice that should behave everywhere the same as a slice but also has some extended functionality. – wim Oct 11 '16 at 06:21
  • 1
    I would upvote this question a couple times more if I could. Since figuring out this is impossible to do, I keep stumbling over use-cases for this left and right... :P – MisterMiyagi Oct 27 '16 at 16:39
  • Yeah, a good enough use-case might convince cpython devs to allow slices to be subclassable in future. It does seem like an arbitrary restriction, that could possibly be lifted at a later date. – wim Oct 27 '16 at 17:05

3 Answers3

8

TLDR: It's impossible to make custom classes replace slice for builtins types such as list and tuple.


The __index__ method exists purely to provide an index, which is by definition an integer in python (see the Data Model). You cannot use it for resolving an object to a slice.

I'm afraid that slice seems to be handled specially by python. The interface requires an actual slice; providing its signature (which also includes the indices method) is not sufficient. As you've found out, you cannot inherit from it, so you cannot create new types of slices. Even Cython will not allow you to inherit from it.


So why is slice special? Glad you asked. Welcome to the innards of CPython. Please wash your hands after reading this.

So slice objects are described in slice.rst. Note these two guys:

.. c:var:: PyTypeObject PySlice_Type

The type object for slice objects. This is the same as :class:slice in the Python layer.

.. c:function:: int PySlice_Check(PyObject *ob) Return true if ob is a slice object; ob must not be NULL.

Now, this is actually implemented in sliceobject.h as :

#define PySlice_Check(op) (Py_TYPE(op) == &PySlice_Type)

So only the slice type is allowed here. This check is actually used in list_subscript (and tuple subscript, ...) after attempting to use the index protocol (so having __index__ on a slice is a bad idea). A custom container class is free to overwrite __getitem__ and use its own rules, but that's how list (and tuple, ...) does it.

Now, why is it not possible to subclass slice? Well, type actually has a flag indicating whether something can be subclassed. It is checked here and generates the error you have seen:

    if (!PyType_HasFeature(base_i, Py_TPFLAGS_BASETYPE)) {
        PyErr_Format(PyExc_TypeError,
                     "type '%.100s' is not an acceptable base type",
                     base_i->tp_name);
        return NULL;
    }

I haven't been able to track down how slice (un)sets this value, but the fact that one gets this error means it does. This means you cannot subclass it.


Closing remarks: After remembering some long-forgotten C-(non)-skills, I'm fairly sure this is not about optimization in the strict sense. All existing checks and tricks would still work (at least those I've found).

After washing my hands and digging around in the internet, I've found a few references to similar "issues". Tim Peters has said all there is to say:

Nothing implemented in C is subclassable unless somebody volunteers the work to make it subclassable; nobody volunteered the work to make the [insert name here] type subclassable. It sure wasn't at the top of my list wink.

Also see this thread for a short discussion on non-subclass'able types.

Practically all alternative interpreters replicate the behavior to various degrees: Jython, Pyston, IronPython and PyPy (didn't find out how they do it, but they do).

Community
  • 1
  • 1
MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
  • 1
    Could you provide a reference for the interface requiring an actual slice, and that tricking it with a subclass somehow is impossible? "It seems you can't do it" is not a particularly convincing answer. – wim Oct 11 '16 at 06:29
  • I'm not saying it seems you can't do it. I'm saying you must provide a slice, and you cannot create new types of slices. Tricking it with a subclass is impossible when you can't create subclasses. Which is to say, I'm looking at the source to see if something definitive turns up. – MisterMiyagi Oct 11 '16 at 06:46
  • This seems to violate duck-typing. If `__getitem__` will accept a slice object (and it does), it should presumably accept something that behaves like a slice. – wim Oct 11 '16 at 06:50
  • @wim I've updated my answer. The gist is: 1) inbuilt containers optimize checking for the slice class. 2) implementing slice subclassing in CPython would require additional work which nobody felt was worth it. Since CPython doesn't allow it, even the interpreters which allow it out of the box added guards against it. Now *that* is frustrating. :/ – MisterMiyagi Oct 11 '16 at 13:50
  • 2
    Well that was enlightening for sure ! Minor edit suggestion: you could add a tl;dr at the begining of your answer to state what appears to be the definitive conclusion "one can't make a class represent itself as a slice when appropriate". – jadsq Oct 11 '16 at 18:54
  • @jadsq Added a TLDR and some small fixes. That post sure grew bigger than planned... – MisterMiyagi Oct 11 '16 at 19:55
5

I'M SORRY FOR THE DARK MAGIC

Using Forbiddenfruit and the python's builtin new method I was able to do this:

from forbiddenfruit import curse


class MyThing(int):
    def __new__(cls, *args, **kwargs):
        magic_slice = slice(args[0], args[1])
        curse(slice, 'otherstuff', args[2])  

        return magic_slice

thing = MyThing(1, 3, 'thing')
print 'hello world'[thing]
print thing.otherstuff

output:

>>> el
>>> thing

I wrote it as a challenge just because everybody said it is impossible, I would never use it on production code IT HAS SO MANY SIDE EFFECTS, you should think again on your structure and needs

Or Duan
  • 13,142
  • 6
  • 60
  • 65
  • Oh yeah, monkeypatching. Hmm. Unfortunately this hack doesn't work with the methods I wanted to muck around with (`__or__`, `__and__`, etc), but have a +1 anyway for the reminder. – wim Oct 11 '16 at 21:55
  • That's changing slice, not creating a new class. One can do `slice(2,3,4).otherstuff` afterwards, and `type(MyThing(1,2,3))` is actually `slice`. Actual instances of `MyThing` still do not act as `slice`s. – MisterMiyagi Oct 12 '16 at 16:45
-2

A slice can't be in your return type as the method just doesn't support this. You can read more about the __index__ special method here. I could only come up with a workaround that directly calls the function in your class:

class MyThing(object):
        def __init__(self, start, stop, otherstuff):
            self.start = start
            self.stop = stop
            self.otherstuff = otherstuff  

        def __index__(self):
            return slice(self.start, self.stop)

    thing = MyThing(1, 3, 'potato')
    print 'Hello World'[thing.__index__()]

This will return el.

Mangohero1
  • 1,832
  • 2
  • 12
  • 20
  • 1
    No, it was called implicitly by the left side's `__getitem__`. Your suggestion is equivalent to just defining an `as_slice` method like `def as_slice(self): return slice(self.start, self.stop)`, which is a workaround that I'm aware of but not what I want. – wim Oct 11 '16 at 05:50
  • If you've read PEP 357 on allowing any object to be used for slicing, you would know the return type of `__index__` *has* to be either `int` or `long`. A slice cannot work there. – Mangohero1 Oct 11 '16 at 06:08
  • 1
    What's the point of calling `__index__`? He could just pass in a slice then - either way it won't be the custom class. – MisterMiyagi Oct 11 '16 at 06:14
  • Well then there you have it. It's just not possible. Although, this *is* a workaround I'm sorry it doesn't quite achieve the effect that you wanted. – Mangohero1 Oct 11 '16 at 06:20
  • No need to apologize. The issue is about how to make an object quack like a slice in whatever way it needs to in order to be accepted by `list.__getitem__`, `str.__getitem__`, and friends. It should have already been evident from the question that `__index__` doesn't work for this. Though, perhaps there are other ways to achieve it. A function which returns a slice does not answer the question. – wim Oct 11 '16 at 06:57