Creating custom lists in Python. Exploring OOP basics to experiment with... | by Roman Penza | Towards Data Science

In this article, we will look at the basics of Object-Oriented Programming and how we can use Python classes to create our own custom lists. We will do so without using any packages (no import statements!).
Let's say we want to create a new data container type that would act as a list (i.e. we can index and slice it) but at the same time could contain data from multiple sources that we can extract just like in a dictionary.
So, say, we have a dictionary of lists:
d ={‘key1’ : [‘val11’, ‘val12’, ‘val13’],    ‘key2’ : [‘val21’, ‘val22’, ‘val23’],    ‘key3’ : [‘val31’, ‘val32’, ‘val33’]}
but we want to be able to easily work with all of the lists in d.values() as if it was one big list:
l = [‘val11’, ‘val12’, ‘val13’,     ‘val21’, ‘val22’, ‘val23’,     ‘val31’, ‘val32’, ‘val33’]
but at the same time, we don’t want to lose the key-value binding of a dictionary. Imagine an object that has these characteristics:
So it is an object that acts as a list but keeps keys for each element in the background. Let's call it a dinx.
To start with, we know that to initiate this object we want a dictionary of lists. Let’s allow tuples as well and say that they will represent static elements that will not change if we mutate the dinx ‘list’. Therefore, we need to assert for input (let’s call it comp, short for components) to be a dictionary and to contain either lists or tuples. The latter can be achieved with either [list, tuple] or (list, tuple) in isinstance second argument. We should also allow only strings in dictionary keys, to avoid confusion when calling numeric indices.
We will assign comp to self and put an underscore prefix to indicate that it is for internal use within the class.
We also need to define a function to get a mapping variable that we can use to get the order of elements in the list representation of the dictionary that we will call _comp_map (will be useful later):
So, self._comp_map is going to look like this:
[(‘key1’, ‘val11’), (‘key1’, ‘val12’), (‘key1’, ‘val13’), (‘key2’, ‘val21’), (‘key2’, ‘val22’), (‘key2’, ‘val23’), (‘key3’, ‘val31’), (‘key3’, ‘val32’), (‘key3’, ‘val33’)]
which is already a list of key-value tuples, but we want to make it pretty-looking.
What we need to do now is to code a dunder method of dinx, called __repr__, that will show the representation of dinx every time we call it. Since we want our dinx to look like a list when we call it, we will start with _sum_lists and _getall methods that we can use to flatten the dictionary of lists we have in _comp.
We can then use _getall output in __repr__ to specify the way dinx is going to look like when we call it in the console:
So, now, we can initiate and call our dinx:
Great! Let’s implement the ability to call values by indices and slices. The idea is that we want to combine dictionary and list indices. So, say an item l[0] in the above example should be a dinx(1), i.e. have a value of 1 and a key ‘A’. A slice l[:5] should be a dinx(1,2,3,0,1) with keys ‘A’ and ‘B’ respectively.
Since we are only allowing string dictionary keys in _comp, we can assume that if it sees string index input, it should return values of the underlying _comp key. Thus, we can combine list and dictionary type of indexing in dinx.
First of all, when we are getting the value of dinx by index we want a dinx to be returned. So, we need to define cls variable as type(self) that we can then use to initiate a new dinx in the return statement:
To generate dinx from int or slice types, we need to define a method that would convert our _comp_map list of tuples to a dictionary that we can use to initiate a new dinx in the return statement:
getting back to __getitem__:
So let’s run a few tests:
In Python, there two most commonly used dunder methods for implementing addition operation: __add__ (+) and __iadd__ (+=). We will start with the latter:
Here, we take two inputs, self and other, where other is another dinx, and we iterate through each element in other._comp and add it to self._comp if the key exists or create a new key in self._comp otherwise. We only do so for lists, thus keeping tuples untouched, which allows us to define elements in our dinx that we don’t want to mutate.
Having defined inplace addition method (x+=y), we can now use it to define x+y type of addition. The idea is pretty simple, we will use a temporary deepcopy of dinx, do an inplace addition of other on it and return the result:
So let’s test and see how it all works:
Another important feature of a list is the ability to sort it. We can also sort lists according to our own functions using key parameter in sort method. In this section, we will create sort method for dinx
The first thing to keep in mind is that we already have a list of key-value tuples that we use to get the order of dinx elements (self._comp_map). Therefore, any sorting function we apply on dinx we can simply pass to self._comp_map. The only problem is that we need to sort by the second element of the tuple. This can be done using lambda x: x[1] as the key. Then, if we want to apply key to the dinx we can apply it to x[1] internally through lambda x: key(x[1]). Lastly, we can apply sorting inplace or make it return a new dinx:
Let’s test our new sort method:
The aim of this article was to show that Python offers fairly simple-to-use tools that we can utilize to configure a project for our needs. An example of a custom data container above is not necessarily applicable in a real-world scenario but nevertheless demonstrates that we can build our own basic data structures to have better control and management over the data being processed.
I have also implemented some smaller things like __contains__ that returns a boolean to x in dinx, __len__ to get the length of dinx, count that returns the count of a variable passed to it, and update, values and keys dictionary methods. Here is the code in full: