Learning Scientific Programming with Python (2nd edition)
P4.6.5: A one-based list
Question P4.6.5
Create a new class that derives from the list
object class which re-implements it with one-based indexing instead of zero-based indexing. Overload as many of the special methods as necessary and write tests to validate your code.
Solution P4.6.5
Here is one approach. The following code defines the class, OneBasedList
implementing one-based indexing on a regular, mutable Python list
. The script after it consists of unit tests (see Chapter 10) to check it works.
class OneBasedList(list):
"""A class implementing a list with one-based indexing."""
def _resolve_index(self, i):
"""A helper function to resolve one-based list indexes to native ones."""
if i is None:
return None
if i == 0:
raise IndexError('No zero index for OneBasedList')
if i < 0:
return i
return i - 1
def _resolve_slice(self, sl):
"""A helper function to resolve one-based list slices to native ones."""
istart = self._resolve_index(sl.start)
istop = sl.stop
if sl.step and sl.step < 0:
istop = self._resolve_index(sl.stop)
return slice(istart, istop, sl.step)
def _resolve_slice_or_index(self, x):
"""
A helper function to resolve one-based list indexes or slices into
native, zero-based ones.
"""
if type(x) is slice:
return self._resolve_slice(x)
return self._resolve_index(x)
def __setitem__(self, i, e):
"""Item setting, either with either a one-based index or slice."""
return super().__setitem__(self._resolve_slice_or_index(i), e)
def __getitem__(self, i):
"""Item getting, either with either a one-based index or slice."""
if type(i) == slice:
# Take care to return another OneBasedList object.
return type(self)(super().__getitem__(self._resolve_slice(i)))
return super().__getitem__(self._resolve_index(i))
def __delitem__(self, i):
"""Delete item at (one-based) index or slice i."""
return super().__delitem__(self._resolve_slice_or_index(i))
def pop(self, i=None):
"""Pop an item from the end of the OneBasedList or at index i."""
if i is None:
return super().pop()
return super().pop(i-1)
def index(self, e, istart=None, istop=None):
"""
Obtain the index of element e, possibly constrained to within the
(one-based) index range istart to istop.
"""
sl = self._resolve_slice(slice(istart, istop, 1))
_istart = sl.start
_istop = sl.stop
if _istop is None:
if _istart is None:
return super().index(e) + 1
return super().index(e, _istart) + 1
return super().index(e, _istart, _istop) + 1
def copy(self):
"""Copy the OneBasedList, returning a new instance of this class."""
return type(self)(self)
def insert(self, i, e):
"""Insert e at (one-based) index i."""
_i = self._resolve_index(i)
super().insert(_i, e)
def __add__(self, other):
"""OneBasedList concatenation, returning another OneBasedList.
Note that adding a list to a OneBasedList returns a OneBasedList;
adding a OneBasedList to a list returns a list.
"""
return type(self)(super().__add__(other))
def __mul__(self, n):
"""OneBasedList concatenation by repetition n times."""
return type(self)(super().__mul__(n))
def __rmul__(self, n):
"""OneBasedList concatenation by repetition n times."""
return type(self)(super().__rmul__(n))
For example,
In [x]: lst1 = OneBasedList('ABC')
In [x]: lst1[1], lst1[2], lst1[3]
Out[x]: ('A', 'B', 'C')
Slicing, insertion, deletion, concatenation, pop
, etc. are all supported.
The test script: (save as one_based_list.py
).
from one_based_list import OneBasedList
import unittest
class TestOneBasedList(unittest.TestCase):
def test_assignment(self):
lst0 = ['a', 'b', 'c']
lst1 = OneBasedList(lst0)
self.assertEqual(str(lst1), str(lst0))
self.assertEqual(lst1[1], 'a')
self.assertEqual(lst1[3], 'c')
self.assertRaises(IndexError, lst1.__getitem__, 0)
self.assertEqual(lst1[-1], lst0[-1])
self.assertEqual(lst1[-3], lst0[-3])
self.assertRaises(IndexError, lst1.__getitem__, -4)
def test_slices(self):
lst0 = list('abcdef')
lst1 = OneBasedList('abcdef')
self.assertEqual(lst0[1:3], lst1[2:3])
self.assertEqual(lst0[1:], lst1[2:])
self.assertEqual(lst0[1:6:2], lst1[2:6:2])
self.assertEqual(lst0[:6], lst1[1:6])
self.assertEqual(lst0[:], lst1[:])
self.assertRaises(IndexError, lst1.__getitem__, slice(0,1))
self.assertEqual(lst0[-1:1:-1], lst1[-1:2:-1])
self.assertEqual(lst0[5:1:-2], lst1[6:2:-2])
self.assertRaises(IndexError, lst1.__setitem__, 0, 'x')
lst0[0] = 'z'
lst1[1] = 'z'
self.assertEqual(lst0, lst1)
lst0[-1] = 'q'
lst1[-1] = 'q'
self.assertEqual(lst0, lst1)
lst0[1:4] = 'STU'
lst1[2:4] = 'STU'
self.assertEqual(lst0, lst1)
lst0[-2::-1] = [1,2,3,4,5]
lst1[-2::-1] = [1,2,3,4,5]
self.assertEqual(lst0, lst1)
lst2 = lst1[:]
self.assertEqual(type(lst2), OneBasedList)
lst2 = lst1[1:3]
self.assertEqual(type(lst2), OneBasedList)
lst3 = OneBasedList('ABCDEFGHIJKL')
self.assertEqual(lst3[3:11:4], ['C','G','K'])
self.assertEqual(lst3[::3], ['A', 'D','G','J'])
self.assertEqual(lst3[::-3], ['L', 'I','F','C'])
def test_delete_items(self):
lst0 = list('abcdef')
lst1 = OneBasedList('abcdef')
del lst0[3]
del lst1[4]
self.assertEqual(lst0, lst1)
del lst0[2:]
del lst1[3:]
self.assertEqual(lst0, lst1)
def test_equality(self):
lst0 = list('ABCD')
lst1 = OneBasedList('ABCD')
self.assertEqual(lst1, lst1)
self.assertEqual(lst0, lst1)
lst2 = OneBasedList('ABCDE')
lst2.pop()
self.assertEqual(lst1, lst2)
def test_index(self):
lst1 = OneBasedList('abcd')
self.assertEqual(lst1.index('a'), 1)
self.assertRaises(ValueError, lst1.index, 'z')
lst2 = OneBasedList('abcdefabcdef')
self.assertEqual(lst2.index('a', 6), 7)
self.assertEqual(lst2.index('a', 6, 7), 7)
def test_pop(self):
lst1 = OneBasedList('abcdefgh')
lst1.pop()
self.assertEqual(lst1, list('abcdefg'))
lst1.pop(3)
self.assertEqual(lst1, list('abdefg'))
def test_copy(self):
lst1 = OneBasedList('abcdefgh')
lst2 = lst1.copy()#[:]
self.assertEqual(type(lst2), OneBasedList)
def test_clear(self):
lst = OneBasedList('ABCD')
lst.clear()
self.assertEqual(type(lst), OneBasedList)
def test_insert(self):
lst0 = list('ABCD')
lst1 = OneBasedList('ABCD')
lst0.insert(1, 'z')
lst1.insert(2, 'z')
self.assertEqual(lst0, lst1)
lst0.insert(-1, 'q')
lst1.insert(-1, 'q')
self.assertEqual(lst0, lst1)
def test_concatenate(self):
lst1 = OneBasedList('abcd')
lst2 = OneBasedList('efgh')
self.assertEqual(lst1 + lst2, list('abcdefgh'))
self.assertEqual(type(lst1 + lst2), OneBasedList)
self.assertEqual(type(lst1 + [1, 2, 3]), OneBasedList)
self.assertEqual(type([1, 2, 3] + lst1), list)
self.assertEqual(lst1*3, list('abcdabcdabcd'))
self.assertEqual(type(lst1*3), OneBasedList)
self.assertEqual(2*lst1, list('abcdabcd'))
self.assertEqual(type(2*lst1), OneBasedList)
def test_extend(self):
lst1 = OneBasedList('abcd')
lst2 = OneBasedList('efgh')
lst1.extend(lst2)
self.assertEqual(lst1, list('abcdefgh'))
self.assertEqual(type(lst1), OneBasedList)
unittest.main()