4

I'm making a container class in Python which will either inherit from list or just implement all the standard list methods (I don't really care which).

How would I create a method that will act on only the items that are returned from a slice? I've been able to make a method that will act on the entire container (see below), but I can't seem to figure out how to act on just the slice.

I'm using python 2.7.6 and use from __future__ import print_function, division in all my code.

Example code:

from __future__ import print_function, division
import itertools

class MyContainerClass(list):
    """ For now, I'm inheriting from list. Is this my problem? """
    def __init__(self, data_array):
        list.__init__(self, data_array)

    def __getitem__(self, *args):

        arg = args[0]

        # specific indices   MyContainerClass[0, 3, 5] => indexes 0, 3, and 5
        if isinstance(arg, (list, tuple)) and not isinstance(arg[0], bool):
            return [list.__getitem__(self, _i) for _i in arg]

        # standard slice notation
        elif isinstance(arg, slice):
            return list.__getitem__(self, arg)

        # Using an array mask   MyContainerClass[[True, False, False,...,True]] => index 0 and -1]
        elif isinstance(arg[0], bool):  # or isinstance(arg, np.ndarray):
            return list(itertools.compress(self, arg))

        else:
            # I'll eventually replace this with and exception raise.
            return 'error'

    def my_method(self):
        """
        Will act on entire list, but I want it to act on only what's
        returned by the slice (which may be the entire list in some cases).
        """
        return "a, ".join([str(_i) for _i in self])

Here is an example of the usage that I'd like:

>>> data = MyContainerClass([1, 2, 3, 4, 5, 6, 7])
>>> data[5:]
[6, 7]
>>> data.my_method()           # This works as expected
"1a, 2a, 3a, 4a, 5a, 6a, 7"
>>> data[0:3].my_method()      # Doesn't work
"1a, 2a, 3"                    # but should return this


Looks like things are working now. Thanks a bunch guys! Here's what I've come up with:

from __future__ import print_function, division
import itertools

class MyContainerClass(list):
    """
    """
    def __init__(self, array):
        if isinstance(array, int):
            list.__init__(self, [array])
        else:
            list.__init__(self, array)

    def __getitem__(self, arg):
        # Standard Slice notation
        if isinstance(arg, slice):
            retval = super(MyContainerClass, self).__getitem__(arg)

        # specific indices
        elif isinstance(arg, (list, tuple)) and not isinstance(arg[0], bool):
            retval = [list.__getitem__(self, _i) for _i in arg]

        # a single specific index
        elif isinstance(arg, int):
            retval = list.__getitem__(self, arg)

        # an array mask of T/F values
        elif isinstance(arg[0], bool):      # or isinstance(arg, np.ndarray):
            retval = list(itertools.compress(self, arg))

        # raise an error on unknown
        else:
            raise SyntaxError("Unknown notation for list slice or index")

        retval = type(self)(retval)
        return retval

    def __getslice__(self, i, j):
        # Python 2 built-in types only
        return self.__getitem__(slice(i, j))

    def my_method(self):
        return "a, ".join([str(_i) for _i in self])

And acts like:

>>> data = MyContainerClass([1, 2, 3, 4, 5, 6, 7])
>>> mask = [True, True, False, True, False, False, False]
>>> print(data)
[1, 2, 3, 4, 5, 6, 7]
>>> print(type(data[5:]))
<class '__main__.MyContainerClass'>
>>> print(data.my_method())
1a, 2a, 3a, 4a, 5a, 6a, 7
>>> print(data[0:5].my_method())
1a, 2a, 3a, 4a, 5
>>> print(data[1, 5, 2].my_method())
2a, 6a, 3
>>> print(data[mask].my_method())
1a, 2a, 4
>>> print(data[2].my_method())
3
dthor
  • 1,749
  • 1
  • 18
  • 45
  • Will the slice again be an instance of that class? If you just inherit that method from `list`, then probably not. And that's the problem: The slice is a `list`, which does not have this method. Alternatively, why not just make it a function instead of a method? – tobias_k Aug 26 '14 at 17:09
  • 2
    Maybe that might be of some interest: http://stackoverflow.com/a/16033058/2363712 ? – Sylvain Leroux Aug 26 '14 at 17:11
  • Is this Python 2 or Python 3? – Martijn Pieters Aug 26 '14 at 17:14
  • @tobias_k: I would like the slice to be an instance of the class, yes. And that should solve my problem. As for function v method: a method is more useful in the grand project design. What you see here is only a simplified example :-) – dthor Aug 26 '14 at 17:15
  • @SylvainLeroux: The actual classes that I'm writing use __getitem__ so that I can accept slice notation, an array mask, or an index list to return elements. As I mentioned in the other comment, it seems like I just need to figure out how to make __getitem__ return an instance of MyContainerClass rather than list. – dthor Aug 26 '14 at 17:16
  • @MartijnPieters Python 2.7 with from `__future__ import print_function, division`. I've edited the post to reflect that. – dthor Aug 26 '14 at 17:17
  • Could you show us how your `__getitem__` looks now? – tobias_k Aug 26 '14 at 17:17
  • @tobias_k: The pose has been updated. – dthor Aug 26 '14 at 17:27

1 Answers1

8

You'll have to make sure your __getitem__ returns your type again:

class MyContainerClass(list):
    """ For now, I'm inheriting from list. Is this my problem? """
    def __init__(self, data_array):
        list.__init__(self, data_array)

    def my_method(self):
        """
        Will act on entire list, but I want it to act on only what's
        returned by the slice (which may be the entire list in some cases).
        """
        return "a, ".join([str(_i) for _i in self])

    def __getitem__(self, index):
        retval = super(MyContainerClass, self).__getitem__(index)
        if isinstance(index, slice):
            retval = type(self)(retval)
        return retval

    def __getslice__(self, i, j):
        # Python 2 built-in types only
        return self.__getitem__(slice(i, j))

The additional __getslice__ method is only needed in Python 2, and then only if you are inheriting from a type that already implements __getslice__. list is such a type.

Demo:

>>> data = MyContainerClass([1, 2, 3, 4, 5, 6, 7])
>>> data[:5]
[1, 2, 3, 4, 5]
>>> type(data[:5])
<class '__main__.MyContainerClass'>
>>> data.my_method()
'1a, 2a, 3a, 4a, 5a, 6a, 7'
>>> data[:3].my_method()
'1a, 2a, 3'
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • I'm not familiar with this line: `retval = type(self)(retval)`. Could you explain it a bit? I've used type(item) before, but not with a 2nd set of () after it. – dthor Aug 26 '14 at 17:33
  • 2
    @dthor: I'm keeping your class subclassable; `type(self)` returns `MyContainerClass` (or any subclass), so you can just call that object again to create a new instance. – Martijn Pieters Aug 26 '14 at 17:34
  • @dthor: you can hardcode the class with `retval = MyContainerClass(retval)`, but then a subclass would have to override `__getitem__` just to alter the returned type. – Martijn Pieters Aug 26 '14 at 17:35
  • 1
    @dthor if that confuses you, you could replace it with `self.__class__(retval)`. They're synonymous. Get the class of `self` and use its constructor to return a new object. – Adam Smith Aug 26 '14 at 19:22
  • @AdamSmith Thanks. I understood it from MartijnPieters comment, but your comment made me realize that it's similar to setting `__class__` of an instance via `self.__class__ = NewClass` (which I do elsewhere in the codebase). I know it's not the same though, as here we're creating a new object. – dthor Aug 26 '14 at 19:26
  • You can also derive a `View` class from `list` that your slice operations return. The instance of `View` would implement the methods you want to apply to slices. – Carl Smith May 10 '18 at 00:34