patternpythonModerate
A Python wrap-around list
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
Goals:
The complete source code with doctests follows:
``
does not
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
listis when explicitly indexed with[].
- Should look and feel like the Python builtin, i.e., wouldn't look too out of place in the
collectionsmodule.
- 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 iterabledoes not
Solution
-
This is well documented, well commented code.
-
The docstring says:
The one way in which slicing a
but this isn't quite the whole story — an ordinary
-
The code is not portable to Python 3, because there's no
I prefer a name like
-
Raising an exception is the right thing to do in this case, but I would expect to get an
-
The code calls
Then
and so on.
-
With a little refactoring, you could avoid some of the repetition. In particular, if you had a method like this:
Then you'd be able to write:
and so on.
-
Once you've done the refactoring above, you'll see that
-
There is no need to include an empty
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 zeroRaising 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):
passThen
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 zeroclass D(WraparoundList, C):
passreturn super(WraparoundList, self).__getitem__(self._wrap_slice(i))Context
StackExchange Code Review Q#153920, answer score: 14
Revisions (0)
No revisions yet.