Search code examples
pythoncollectionssubclassing

Why a UserList subclass appears to return the wrong type for slicing?


Let me start by saying I get why subclassing list doesn't work as you might expect (because list is a primitive built-in type and there are issues of performance, etc.). AFAIK collections.UserList is supposed to avoid all these problems and make subclassing UserList work completely as you might expect. For example,

class DumbList(list):
  pass
d1 = DumbList([1,2,3])
d2 = DumbList([4,5])
type(d1 + d2)

returns <class 'list'>, but

from collections import UserList
class DumbList(UserList):
  pass
d1 = DumbList([1,2,3])
d2 = DumbList([4,5])
type(d1 + d2)

returns <class '__main__.DumbList'> as expected. However, slicing appears to return the wrong type, even when using UserList instead of list:

class DumbList(UserList):
  pass
d = DumbList([1,2,3])
type(d[:2])

returns <class 'list'>, not <class '__main__.DumbList'> as expected.

Two questions:

  • Why is this?
  • What should I do to make slicing return the correct type? The simplest thing I can think of is something like:

 

class DumbList(UserList):
  def __getitem__(self, item):
    result = UserList.__getitem__(self, item)
    try:
      return self.__class__(result)
    except TypeError:
      return result

...but it seems like this kind of boiler-plate code should be unnecessary.


Solution

  • In Python 2, plain slicing (without a stride) would be handled by the __getslice__ method. The UserList implementation predates the addition of extended slices (with stride) to the language and never added support for them, see issue 491398.

    The Python 3 implementation simply took the Python 2 version, moved into collections and removed __getslice__ and __setslice__ as those are no longer supported in Python 3.

    As such, the __getitem__ implementation is still simply:

    def __getitem__(self, i): return self.data[i]
    

    assuming that slicing would be handled elsewhere.

    In Python 3, all slicing is handled by passing in the slice() built-in type to __getitem__; simply test for that type and wrap the result in a type(self) call:

    def __getitem__(self, i):
        res = self.data[i]
        return type(self)(res) if isinstance(i, slice) else res