Special Methods and Linked Lists

A list is a container for a sequence of values. Recall that “sequence” implies an order.

Another way to think about this: A list is a chain of values, or a linked list.

Each value in the list has something after it: the rest of the sequence. (Recursion!)

How do we know when we reach the end of our list? When the rest of the list is None.

class LinkedList:
    """Implements our own recursive list data structure"""
    __slots__ = ['_value', '_rest']

    def __init__(self, value=None, rest=None):
        self._value = value
        self._rest = rest
        
myList = LinkedList(5, LinkedList(3, LinkedList(11)))
type(myList)
__main__.LinkedList
len(myList) # will this work?
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/md/kwd9nc_d2ns0hw9wsvdrnt2c0000gn/T/ipykernel_85918/2939764030.py in <module>
----> 1 len(myList) # will this work?

TypeError: object of type 'LinkedList' has no len()

Special Methods

Suppose we want to support the following built-in functions: len(), str(), contains(), add(), getitem(), setitem(), and eq(). We might also want to add the method append() for consistency with regular lists.

class LinkedList:
    """Implements our own recursive list data structure"""
    __slots__ = ['_value', '_rest']

    def __init__(self, value=None, rest=None):
        self._value = value
        self._rest = rest
       
    # getters/setters
    def getRest(self):
        return self._rest
    
    def getValue(self):
        return self._value

    def setValue(self, val):
        self._value = val
        
    def __strElements(self):
        # helper function for __str__()
        if self._rest is None:
            return str(self._value)
        else:
            return str(self._value) + ", " + self._rest.__strElements()
        
    def __str__(self):
        return "[" + self.__strElements() + "]"
    
    # repr() function calls __repr__() method
    # return value should be a string that is a valid Python 
    # expression that can be used to recreate the LinkedList
    def __repr__(self):
        return "LinkedList({}, {})".format(self._value, repr(self._rest))

    # len() function calls __len__() method
    def __len__(self):
        # base case: i'm the last item
        if self._rest is None:
            return 1
        else:
            # same as return 1 + self.rest.__len__()
            return 1 + len(self._rest)  
    
    # in operator calls __contains__() method
    def __contains__(self, val):
        if self._value == val:
            return True
        elif self._rest is None:
            return False
        else:
            # same as calling self.__contains__(val)
            return val in self._rest
        
    # + operator calls __add__() method
    # + operator returns a new instance of LinkedList
    def __add__(self, other):
        # other is another instance of LinkedList
        # if we are the last item in the list
        if self._rest is None:
            # set _rest to other
            self._rest = other
        else:
            # else, recurse until we reach the last item
            self._rest.__add__(other)
        return self
 
    # [] list index notation calls __getitem__() method
    # index specifies which item we want
    def __getitem__(self, index):
        # if index is 0, we found the item we need to return
        if index == 0:
            return self._value
        else:
            # else we recurse until index reaches 0
            # remember that this implicitly calls __getitem__
            return self._rest[index - 1]
        
    # [] list index notation also calls __setitem__() method
    # index specifies which item we want, val is new value
    def __setitem__(self, index, val):
        # if index is 0, we found the item we need to update
        if index == 0:
            self._value = val
        else:
            # else we recurse until index reaches 0
            # remember that this implicitly calls __setitem__
            self._rest[index - 1] = val
            
    # == operator calls __eq__() method
    # if we want to test two LinkedLists for equality, we test 
    # if all items are the same
    # other is another LinkedList
    def __eq__(self, other):
        # If both lists are empty
        if self._rest is None and other.getRest() is None:
            return True

        # If both lists are not empty, then value of current list elements 
        # must match, and same should be recursively true for 
        # rest of the list
        elif self._rest is not None and other.getRest() is not None :
            return self._value == other.getValue() and self._rest == other.getRest()

        # If we reach here, then one of the lists is empty and other is not
        return False
   
    # append is not a special method, but it is a method
    # that we know and love from the Python list class.
    # unlike __add__, we do not return a new LinkedList instance
    def append(self, val):
        # if am at the list item
        if self._rest is None:
            # add a new LinkedList to the end
            self._rest = LinkedList(val)
        else:
            # else recurse until we find the end
            self._rest.append(val)
            
myList = LinkedList(4)
print(myList)
[4]
myList = LinkedList(5, myList)
print(myList)
[5, 4]
myList = LinkedList(5, LinkedList(3, LinkedList(11)))
print(myList) # testing __str__
[5, 3, 11]
myList  # testing __repr__
LinkedList(5, LinkedList(3, LinkedList(11, None)))
len(myList)  # testing __len__
3
1 in myList # testing __contains__
False
11 in myList
True
myList2 = LinkedList(4, LinkedList(6, LinkedList(10)))
print(myList2)
[4, 6, 10]
print(myList)
[5, 3, 11]
myList3 = myList + myList2  # testing __add__
print(myList3)
[5, 3, 11, 4, 6, 10]
len(myList3)
6
print(myList3[0])  # testing __getitem__
5
myList3[0]=7   # testing __setitem__
print(myList3)
[7, 3, 11, 4, 6, 10]
myList4 = LinkedList(5, LinkedList(3, LinkedList(11, LinkedList(13))))
myList5 = LinkedList(5, LinkedList(3, LinkedList(11)))
myList6 = LinkedList(5, LinkedList(3, LinkedList(11)))
print(myList4)
print(myList5)
print(myList6)
[5, 3, 11, 13]
[5, 3, 11]
[5, 3, 11]
myList4 == myList5  # testing __eq__
False
myList5 == myList6 
True
myList6.append(20)  # testing append
myList = LinkedList(5, LinkedList(3, LinkedList(11)))
for item in myList:
    print(item)
5
3
11
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/md/kwd9nc_d2ns0hw9wsvdrnt2c0000gn/T/ipykernel_85918/858435769.py in <module>
----> 1 for item in myList:
      2     print(item)

/var/folders/md/kwd9nc_d2ns0hw9wsvdrnt2c0000gn/T/ipykernel_85918/1700578427.py in __getitem__(self, index)
     74             # else we recurse until index reaches 0
     75             # remember that this implicitly calls __getitem__
---> 76             return self._rest[index - 1]
     77 
     78     # [] list index notation also calls __setitem__() method

/var/folders/md/kwd9nc_d2ns0hw9wsvdrnt2c0000gn/T/ipykernel_85918/1700578427.py in __getitem__(self, index)
     74             # else we recurse until index reaches 0
     75             # remember that this implicitly calls __getitem__
---> 76             return self._rest[index - 1]
     77 
     78     # [] list index notation also calls __setitem__() method

/var/folders/md/kwd9nc_d2ns0hw9wsvdrnt2c0000gn/T/ipykernel_85918/1700578427.py in __getitem__(self, index)
     74             # else we recurse until index reaches 0
     75             # remember that this implicitly calls __getitem__
---> 76             return self._rest[index - 1]
     77 
     78     # [] list index notation also calls __setitem__() method

TypeError: 'NoneType' object is not subscriptable