Structuring data in Python (Part 1)

Nesting in Python

Example 1

One simple way to structure data is to use built-in structures in new ways, such as nesting one inside another. For example, we can make a nested list by having lists as elements of lists. Let's consider a concrete example to illustrate this point. Running the code

nest = [1, [2, 3, [4]], [5, [6, [7]]]]
print(len(nest))

The length of this list is three, as it consists of three items, the first being the number 1, the second being the list [2, 3, [4]], and the third being the list [5, [6, [7]]], which contains 5 and the list [6, [7]]. This is confirmed by the output 3 when the code is run.


print(5 in nest)

Although 5 is an item in an item, it is not an item in our list, which is indicated by the output, False, when the code is run.


print(nest[1][2])

Remember, the three items are the number one and two lists. We can use index notation repeatedly to extract items. For example, nest[1] extracts the list [2, 3, [4]], and nest[1][2] then extracts the list [4]. This is confirmed by the output, [4], when the code is run.


print(nest[1][2][0])

To extract 4 itself, we need to use another index, nest[1][2][0], to extract a list item. This is confirmed by the output, 4, produced when the code is run.


Here's the complete sequence of statements and their outputs:

nest = [1, [2, 3, [4]], [5, [6, [7]]]]
print(len(nest))
print(5 in nest)
print(nest[1][2])
print(nest[1][2][0])
3
False
[4]
4

Example 2

As another example, consider a list of objects and extracting a particular attribute in an object:

class Time:
    def __init__(self, hour, minute):
        self.hour = hour
        self.minute = minute
 
one = Time(1, 0)
two = Time(2, 0)
three = Time(3, 0)

time_list = [one, two, three]

print(time_list[1].hour)

To extract a particular attribute in an object, we first use the index to extract an object from a list. In the statement print(time_list[1].hour), we extract the object in position 1, which is the time object named two. Next we use dot notation to extract the value of the attribute. The value of hour in the time object named two is the number 2, which is what we get when the code is run.


Example 3

We can also use a list as one of the attributes of an object:

class Mystery:
    def __init__(self, name, num_list):
        self.name = name
        self.num_list = num_list

one = Mystery("first", [10, 11, 12])

Here we have an object that has two attributes, namely, a string and a list of numbers.


print(one.num_list[1])

To extract the number 11, we first use dot notation to extract the list and then the index to extract the item. Running this code gives 11, as expected.


import copy
two = copy.copy(one)
print(two is one)

Remember the problem we had with copying objects inside objects? Running this code gives the output False.


one.num_list[1] = 100
print(one.num_list[1])
print(two.num_list[1])

We have the same problem here in that the new object is using the same list as the old object, and hence any change to this list changes the list in both. Running this code gives the output 100 for both the print statements. deepcopy will avoid this problem.


Here's the complete sequence of statements and their outputs:

class Mystery:
    def __init__(self, name, num_list):
        self.name = name
        self.num_list = num_list

one = Mystery("first", [10, 11, 12])

print(one.num_list[1])

import copy
two = copy.copy(one)
print(two is one)

one.num_list[1] = 100
print(one.num_list[1])
print(two.num_list[1])
11
False
100
100

Summary

To summarize, we can use any of the following ways of nesting structures for data, plus many more:

  • lists of lists
  • lists of objects
  • lists in objects

Caution

Pay attention to notation to access elements.

Caution

Remember to use deepcopy.

In accessing elements, keep in mind what you have extracted so far. If it is a list, use an index. If it is an object, use dot notation. Also, use deepcopy to avoid aliasing.


Flattening a list of lists

As an example of a function that handles a list of lists, suppose we wish to extract all the items and form a simple list.

def flatten(list_list):

We have our list.


    flat = []

We initialize the list that we use to store the simple list.


    for each in list_list:

To iterate through all the lists, we use a for loop.


        for item in each:

We use another for loop to iterate through the items in each of the lists. This means that we will first examine all the items in the first list, then all the items in the second list, and so on.


            flat.append(item)

We will append each item in turn onto a list that we create.


    return flat

Finally, we return the output.


def test_flatten():
    assert flatten([[1, 2], [3, 4]]) \
     == [1, 2, 3, 4]
    assert flatten([[], [2], [1, 2]]) == [2, 1, 2]
    assert flatten([[], []]) == []
    assert flatten([]) == []

test_flatten()

To test our function, we make use of examples where the main list is empty as well as when one or more of the lists of items is empty.


Here's our function when all parts are put together:

def flatten(list_list):
    flat = [] 
    for each in list_list:
        for item in each:
            flat.append(item)
    return flat

def test_flatten():
    assert flatten([[1, 2], [3, 4]]) \
     == [1, 2, 3, 4]
    assert flatten([[], [2], [1, 2]]) == [2, 1, 2]
    assert flatten([[], []]) == []
    assert flatten([]) == []

test_flatten()