HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonModerate

A Python wrap-around list

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
aroundlistwrappython

Problem

I want to gain experience creating data structures that look and feel like Python builtin types. As a first exercise, I've written a WraparoundList class meant to be identical to the builtin list, except that accessing out-of-bounds elements "wraps around".

Goals:

  • The only behavior that differs from that of a list is when explicitly indexed with [].



  • Should look and feel like the Python builtin, i.e., wouldn't look too out of place in the collections module.



  • Should be compatible with both Python 2.7.x and 3.x (though I only have tested on 2.7.13).



The complete source code with doctests follows:

``
#!/usr/bin/env python

from sys import maxint as MAXINT

class WraparoundList(list):
"""A list whose index wraps around when out of bounds.

A
WraparoundList is the same as an ordinary list,
except that out-of-bounds indexing causes the index
value to wrap around. The wrapping behavior is as if
after reaching the last element, one returned to the
other end of the list and continued counting.

>>> x = WraparoundList('abcd')
>>> x
['a', 'b', 'c', 'd']
>>> x[3]
'd'
>>> x[4] # wraps to x[0]
'a'
>>> x[-6] = 'Q' # wraps to x[-2]
>>> x
['a', 'b', 'Q', 'd']
>>> del x[7] # wraps to x[3]
>>> x
['a', 'b', 'Q']

Indices used in out-of-range slices also wrap around.
If the slice's
start or stop is out-of-bounds, it
gets wrapped around.

>>> x = WraparoundList('abcd')
>>> x
['a', 'b', 'c', 'd']
>>> x[:10] # wraps to x[:2]
['a', 'b']
>>> x[-7:3] # wraps to x[-3:3]
['b', 'c']

The one way in which slicing a
WraparoundList differs
from slicing an ordinary
list is the case of using the
list length as the upper limit.

>>> x = WraparoundList('abcd')
>>> x
['a', 'b', 'c', 'd']
>>> x[2:]
['c', 'd']
>>> x[2:4] # wraps to x[2:0]
[]

Initializing a
WraparoundList` with a nested iterable
does not

Solution

-
This is well documented, well commented code.

-
The docstring says:


The one way in which slicing a WraparoundList differs from slicing an ordinary list is the case of using the list length as the upper limit.

but this isn't quite the whole story — an ordinary list can also be sliced using a value greater than the list length, and in that case WraparoundList also has a different behaviour:

>>> x = [1, 2, 3]
>>> x[:10]
[1, 2, 3]
>>> x = WraparoundList(x)
>>> x[:10]
[1]


-
The code is not portable to Python 3, because there's no sys.maxint (all integers in Python 3 are "long"). I suggest something like this:

try:
    # In Python 2.7, when __*slice__ methods are called with no "stop"
    # value, sys.maxint is passed instead.
    from sys import maxint as NO_STOP
except ImportError:
    # Python 3 does not have sys.maxint or use the __*slice__ methods.
    NO_STOP = object()


I prefer a name like NO_STOP because it communicates the intention rather than the implementation.

-
_wrap_index raises ZeroDivisionError if the list is empty:

>>> w = WraparoundList([])
>>> w[0]
Traceback (most recent call last):
  File "", line 1, in 
  File "cr153920.py", line 79, in __getitem__
    return list.__getitem__(self, self._wrap_index(i))
  File "cr153920.py", line 110, in _wrap_index
    return i % _len
ZeroDivisionError: integer division or modulo by zero


Raising an exception is the right thing to do in this case, but I would expect to get an IndexError instead.

-
The code calls list.__getitem__ directly, rather than via the super function. But this has the unsatisfactory consequence that if someone has another class C also inheriting from list and overriding the __getitem__ method, and combines WraparoundList and C via inheritance, like this:

class D(WraparoundList, C):
    pass


Then D()[0] calls WraparoundList.__getitem__, which calls list.__getitem__, but C.__getitem__ is never called, contrary to what one would expect. If you want to support subclassing of WraparoundList, then you need to write:

return super(WraparoundList, self).__getitem__(self._wrap_slice(i))


and so on.

-
With a little refactoring, you could avoid some of the repetition. In particular, if you had a method like this:

def _wrap_arg(self, i):
    if isinstance(i, slice):
        return self._wrap_slice(i)
    else:
        return self._wrap_index(i)


Then you'd be able to write:

def __getitem__(self, i):
    """x.__getitem__(i)  x[i]"""
    return super(WraparoundList, self).__getitem__(self._wrap_arg(i))


and so on.

-
Once you've done the refactoring above, you'll see that _wrap_slice is only called from one place, so it could be inlined at its point of use.

-
There is no need to include an empty main function or an if __name__ == '__main__': section — if there's nothing to do, then there's no need to write code to do it.

Code Snippets

>>> x = [1, 2, 3]
>>> x[:10]
[1, 2, 3]
>>> x = WraparoundList(x)
>>> x[:10]
[1]
try:
    # In Python 2.7, when __*slice__ methods are called with no "stop"
    # value, sys.maxint is passed instead.
    from sys import maxint as NO_STOP
except ImportError:
    # Python 3 does not have sys.maxint or use the __*slice__ methods.
    NO_STOP = object()
>>> w = WraparoundList([])
>>> w[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "cr153920.py", line 79, in __getitem__
    return list.__getitem__(self, self._wrap_index(i))
  File "cr153920.py", line 110, in _wrap_index
    return i % _len
ZeroDivisionError: integer division or modulo by zero
class D(WraparoundList, C):
    pass
return super(WraparoundList, self).__getitem__(self._wrap_slice(i))

Context

StackExchange Code Review Q#153920, answer score: 14

Revisions (0)

No revisions yet.