Open In Colab

1.3. Data Structure, Functions, and Classes#

In this notebook, we will be discovering how data is stored in Python. We will cover:

  1. Lists

  2. Tuples

  3. Dictionaries

Reference:

1.3.1. Lists#

Creating lists is very easy in Python. We can create lists by separating different items with commas in square brackets:
[Item1,Item2,Item3]

There are many different ways to interact with lists. Exploring them is part of the fun of Python.

list.append(x) Add an item to the end of the list. Equivalent to a[len(a):] = [x].

list.extend(L) Extend the list by appending all the items in the given list. Equivalent to a[len(a):] = L.

list.insert(i, x) Insert an item at a given position. The first argument is the index of the element before which to insert, so a.insert(0, x) inserts at the front of the list, and a.insert(len(a), x) is equivalent to a.append(x).

list.remove(x) Remove the first item from the list whose value is x. It is an error if there is no such item.

list.pop([i]) Remove the item at the given position in the list, and return it. If no index is specified, a.pop() removes and returns the last item in the list. (The square brackets around the i in the method signature denote that the parameter is optional, not that you should type square brackets at that position. You will see this notation frequently in the Python Library Reference.)

list.clear() Remove all items from the list. Equivalent to del a[:].

list.index(x) Return the index in the list of the first item whose value is x. It is an error if there is no such item.

list.count(x) Return the number of times x appears in the list.

list.sort() Sort the items of the list in place.

list.reverse() Reverse the elements of the list in place.

list.copy() Return a shallow copy of the list. Equivalent to a[:].

# Let's experiment with some of these methods!
# 1. Create a list first
l = ['dog', 'cat', 'fish','chicken','eggs','duck']
type(l)
list
# Slicing the list
print(l[0],l[-1],l[0:3],l[-3:-1])
dog duck ['dog', 'cat', 'fish'] ['chicken', 'eggs']

Consider memorizing this syntax! It is central to so much of Python and often proves confusing for users coming from other languages.

1.3.1.1. Updating list#

Unlike strings and tuples, lists are mutable. You can update the list and change the items whenever you want.

my_list = ['dog', 'cat', 'fish','chicken','eggs','duck']
my_list[2] = 'Tree'
print('Original Contents: \n', my_list)
print('Original Length of array: \n', len(my_list))

# Remove some elements/ changing the size
my_list[2:4] = []
print('Modified Contents: \n', my_list)
print('Modified Length of array: \n', len(my_list))
Original Contents: 
 ['dog', 'cat', 'Tree', 'chicken', 'eggs', 'duck']
Original Length of array: 
 6
Modified Contents: 
 ['dog', 'cat', 'eggs', 'duck']
Modified Length of array: 
 4

Let modify the list with some list specific methods we have discussed earlier

#@title  #### **Add new items to list**
my_list = ['dog', 'cat', 'fish','chicken','eggs','duck']
my_list.append('Python') # It will append the item to the end of the list
print(my_list)
my_list.insert(0, 'Julia')
print(my_list)
['dog', 'cat', 'fish', 'chicken', 'eggs', 'duck', 'Python']
['Julia', 'dog', 'cat', 'fish', 'chicken', 'eggs', 'duck', 'Python']
#@title  #### **Remove list items**
my_list.pop(0)
print(my_list)
del(my_list[-1])
print(my_list)
['dog', 'cat', 'fish', 'chicken', 'eggs', 'duck', 'Python']
['dog', 'cat', 'fish', 'chicken', 'eggs', 'duck']
#@title #### **Count, Index**
print(my_list.count('fish'))
print(my_list.index('duck'))
1
5
#@title #### **Sort, Reverse**
my_list = ['dog', 'cat', 'fish','chicken','eggs','duck']
my_list.reverse()
print(my_list)
my_list.sort()
print(my_list)
my_list.sort(reverse=True)
print(my_list)
['duck', 'eggs', 'chicken', 'fish', 'cat', 'dog']
['cat', 'chicken', 'dog', 'duck', 'eggs', 'fish']
['fish', 'eggs', 'duck', 'dog', 'chicken', 'cat']
#@title #### **List Concatenation**
my_list = ['dog', 'cat', 'fish','chicken','eggs','duck']
my_list2 = ['Python','Julia','C++']
print(my_list+my_list2)
my_list.extend(my_list2)
print(my_list)
['dog', 'cat', 'fish', 'chicken', 'eggs', 'duck', 'Python', 'Julia', 'C++']
['dog', 'cat', 'fish', 'chicken', 'eggs', 'duck', 'Python', 'Julia', 'C++']

1.3.1.2. Use lists in loops#

for index in range(len(my_list)): # start from 0 and go till the length of the list.
    print("my_list[{}] : {}".format(index, my_list[index]))
my_list[0] : dog
my_list[1] : cat
my_list[2] : fish
my_list[3] : chicken
my_list[4] : eggs
my_list[5] : duck
my_list[6] : Python
my_list[7] : Julia
my_list[8] : C++
# We can actually do the same thing with enumerate()
for index,items in enumerate(my_list):
  print("my_list[{}] : {}".format(index,items))
my_list[0] : dog
my_list[1] : cat
my_list[2] : fish
my_list[3] : chicken
my_list[4] : eggs
my_list[5] : duck
my_list[6] : Python
my_list[7] : Julia
my_list[8] : C++

List Comprehension

List comprehension is a syntactic way of creating a list based on the existing list, just like we did when copying the lists above. The basic structure of the syntax includes a for loop that traverses the list and evaluates a condition using the if.. else condition. It then stores the output of the condition as a new list. Let’s take a look at a quick example:

my_list1 = [elem for index,elem in enumerate(my_list) if index % 2 == 0]
print(my_list1)

squares = [n**2 for n in range(5)]
print(squares)
['dog', 'fish', 'eggs', 'Python', 'C++']
[0, 1, 4, 9, 16]

Traverse two lists together with zip()

# iterate over two lists together uzing zip
for item1, item2 in zip(['pig','duck','butterfly'],[0,1,2]):
    print('first:', item1, 'second:', item2)
first: pig second: 0
first: duck second: 1
first: butterfly second: 2

1.3.2. Tuples#

Tuples are similar to lists, but they are immutable—they can’t be extended or modified. What is the point of this? Generally speaking: to pack together inhomogeneous data. Tuples can then be unpacked and distributed by other parts of your code.

Tuples may seem confusing at first, but with time you will come to appreciate them.

# tuples are created with parentheses, or just commas
a = ('Ryan', 33, True)
b = 'Takaya', 25, False
type(b)
tuple
# can be indexed like arrays
print(a[1]) # not the first element!
33
# and they can be unpacked
name, age, status = a
print(name,age,status)
Ryan 33 True
print(a.index('Ryan'))
0

1.3.3. Dictionaries#

This is an extremely useful data structure. It maps keys to values.

Dictionaries are unordered!

#@title #### **Different ways to create dictionaries**
d = {'name': 'Timothee', 'age': 1} # Write your name and your age here
e = dict(name='Timothee', age=1) # Write someone else's name and their age here
#@title #### **Access Dictionary Items**
print(d['name'])
print("name:   ", d.get('name'  , 'Not Found'))
print("gender:   ", d.get('gender', 'Not Found'))
Timothee
name:    Timothee
gender:    Not Found
#@title #### **Updating Dictionary**
# Add a new variable-value pair:
d['height'] = (1,50) # a tuple, e.g. your height in (meters,centimeters)
d
{'name': 'Timothee', 'age': 1, 'height': (1, 50)}
#@title #### **Append Dictionary**
# update() add two dictionary together
newdict = dict(location='Lausanne',nationality='CH',birth_year='2021')
d.update(newdict)
print(d)
{'name': 'Timothee', 'age': 1, 'height': (1, 50), 'location': 'Lausanne', 'nationality': 'CH', 'birth_year': '2021'}
#@title #### **Remove Elements from Dictionary**
# pop(),del()
d2 = d.copy()
d2.pop('age')
del d['age']
print(f"Identical? {d==d2}")
Identical? True
#@title #### **Traverse Dictionary**
# iterate over keys
for k in d:
    print(k, d[k])
name Timothee
height (1, 50)
location Lausanne
nationality CH
birth_year 2021
# better way
for key, val in d.items():
    print(key, val)
name Timothee
height (1, 50)
location Lausanne
nationality CH
birth_year 2021
print(list(d.keys()))
print(list(d.values()))
print(list(d.items()))
['name', 'height', 'location', 'nationality', 'birth_year']
['Timothee', (1, 50), 'Lausanne', 'CH', '2021']
[('name', 'Timothee'), ('height', (1, 50)), ('location', 'Lausanne'), ('nationality', 'CH'), ('birth_year', '2021')]
#@title #### **Sort Dictionary**
# sorted()
states_dict = {'AL': 'Alabama', 'CA': 'California',
               'NJ': 'New Jersey', 'NY': 'New York'}
sorted_keys = sorted(list(states_dict.keys()), reverse=False)
for key in sorted_keys:
    print('{} : {}'.format(key, states_dict[key]))
AL : Alabama
CA : California
NJ : New Jersey
NY : New York

1.3.4. Functions and Classes#

For longer and more complex tasks, it is important to organize your code into reuseable elements. For example, if you find yourself cutting and pasting the same or similar lines of code over and over, you probably need to define a function to encapsulate that code and make it reusable. An important principle in programming in DRY: “don’t repeat yourself”. Repetition is tedious and opens you up to errors. Strive for elegance and simplicity in your programs.

We can use the following two things to organize our code:

  1. Functions

  2. Classes

Reference:

1.3.4.1. Functions#

Functions are a central part of advanced python programming. Functions take some inputs (“arguments”) and do something in response. Usually functions return something, but not always.

#@title **Define a Function**
def say_hello():
    """Return the word hello."""
    return 'Hello'

def say_hello_to(name=None):
  """
  Return the word hello to someone
  """
  return 'Hello, '+str(name)

# take an optional keyword argument
def say_hello_or_hola(name, french=False):
    """Say hello in multiple languages."""
    if french:
        greeting = 'Bonjour '
    else:
        greeting = 'Hello '
    return greeting + name

# flexible number of arguments
def say_hello_to_everyone(*args):
    return ['Bonjour ' + str(a) for a in args]
print(say_hello())
res = say_hello()
print(res)
Hello
Hello
print(say_hello_or_hola('Frédéric', french=True)) # Greet the rector
print(say_hello_or_hola('Frédéric', french=False))
print(say_hello_to_everyone('Niklas', 'Valérie', 'Marie-Élodie')) # Greet the deans
Bonjour Frédéric
Hello Frédéric
['Bonjour Niklas', 'Bonjour Valérie', 'Bonjour Marie-Élodie']
#@title **Anonymous Function**
mul = lambda a, b: a*b
print(mul(4,5))
20
#@title **Map Function**
# syntax: map(function,iterator)
numbers = range(1, 10)
def square(num):
    return num**2
list(map(square, numbers))
# The equivalent of this function is:
# result = []
# for i in range(1, 10):
#     result.append(i**2)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
# We can also write this in a single line!
list(map(lambda x: x**2, range(1, 10)))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
# Return all values for which %2 is non zero.. (List of all odd numbers, right?)
list(filter(lambda x: x%2, range(1, 10)))
[1, 3, 5, 7, 9]

1.3.4.1.1. Pure vs Impure Functions#

Functions that don’t modify their arguments or produce any other side-effects are called pure.

Functions that modify their arguments or cause other actions to occur are

# Impure Functions
def remove_last_from_list(input_list):
    input_list.pop()
names = ['Niklas', 'Valérie', 'Marie-Élodie']
remove_last_from_list(names)
print(names)
remove_last_from_list(names)
print(names)
['Niklas', 'Valérie']
['Niklas']
#We can do something similar with a pure function. In general, pure functions are safer and more reliable.
def remove_last_from_list_pure(input_list):
    new_list = input_list.copy()
    new_list.pop()
    return new_list

names = ['Niklas', 'Valérie', 'Marie-Élodie']
new_names = remove_last_from_list_pure(names)
print(names)
print(new_names)
['Niklas', 'Valérie', 'Marie-Élodie']
['Niklas', 'Valérie']

1.3.4.1.2. Namespace#

In python, a namespace is a mapping between variable names and python object. You can think of it like a dictionary. The namespace can change depending on where you are in your program. Functions can “see” the variables in the parent namespace, but they can also redefine them in a private scope.

It’s important that you be aware of the name spaces in your code, specially when dealing with mutable objects.

name = 'Tom' # Enter your name here
def print_name():
    print(name)

def print_name_v2():
    name = 'Estelle'
    print(name)

print_name()
print_name_v2()
print(name)
Tom
Estelle
Tom
friends_list = ['Mario', 'Florence', 'Richard']
pet_tuple = ('Hedwig', 'Na-paw-lyon', 'Cat-hilda')
def greeter(friends, pets):
    print("It's time to say hi to my friends.")
    [print(f'Hi {name}! ', end="") for name in friends]
    print('\nThese are the names of my pets:')
    [print(f'{pet} ', end="") for pet in pets]
    print('\n')

def pets_are_friends(friends, pets):
    print("I consider both my pets and my friend's pets my friends!")

    #add friend's pets
    full_pets = pets
    full_pets += ('Clifford', 'Crookshanks')

    full_friends_list = friends
    full_friends_list.extend(full_pets)

    print('These are all my friends:')
    [print(f'{name} ', end="") for name in full_friends_list]
    print('\n')
greeter(friends_list, pet_tuple)
pets_are_friends(friends_list, pet_tuple)
greeter(friends_list, pet_tuple)
It's time to say hi to my friends.
Hi Mario! Hi Florence! Hi Richard! 
These are the names of my pets:
Hedwig Na-paw-lyon Cat-hilda 

I consider both my pets and my friend's pets my friends!
These are all my friends:
Mario Florence Richard Hedwig Na-paw-lyon Cat-hilda Clifford Crookshanks 

It's time to say hi to my friends.
Hi Mario! Hi Florence! Hi Richard! Hi Hedwig! Hi Na-paw-lyon! Hi Cat-hilda! Hi Clifford! Hi Crookshanks! 
These are the names of my pets:
Hedwig Na-paw-lyon Cat-hilda 

1.3.4.2. Classes#

We have worked with many different types of python objects so far: strings, lists, dictionaries, etc. These objects have different attributes and respond in different ways to the built-in functions (len, etc.)

How can we make our own, custom objects? Answer: by defining classes.

1.3.4.2.1. A class to represent a hurricane#

Let’s create our first class below,

class Hurricane:

    def __init__(self, name):
        self.name = name

And now let’s create an instance of that class, h:

h = Hurricane('florence')
print(h,h.name)
<__main__.Hurricane object at 0x00000246ABBB1DC0> florence

Let’s add more, along with some input validation:

class Hurricane:

    def __init__(self, name, category, lon):
        self.name = name.upper()
        self.category = int(category)

        if lon > 180 or lon < -180:
            raise ValueError(f'Invalid lon {lon}')
        self.lon = lon
h = Hurricane('florence', 4, -46)
print(h,h.name)
h = Hurricane('Ida', 5, 300)
<__main__.Hurricane object at 0x00000246ABB6B9D0> FLORENCE
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[42], line 3
      1 h = Hurricane('florence', 4, -46)
      2 print(h,h.name)
----> 3 h = Hurricane('Ida', 5, 300)

Cell In[41], line 8, in Hurricane.__init__(self, name, category, lon)
      5 self.category = int(category)
      7 if lon > 180 or lon < -180:
----> 8     raise ValueError(f'Invalid lon {lon}')
      9 self.lon = lon

ValueError: Invalid lon 300
# Now let’s add a custom method:
class Hurricane:

    def __init__(self, name, category, lon):
        self.name = name.upper()
        self.category = int(category)

        if lon > 180 or lon < -180:
            raise ValueError(f'Invalid lon {lon}')
        self.lon = lon

    def is_dangerous(self):
        return self.category > 1
f = Hurricane('florence', 4, -46)
f.is_dangerous()

1.3.4.2.2. Magic / Dunder Methods#

We can implement special methods that begin with double-underscores (i.e. “dunder” methods), which allow us to customize the behavior of our classes. (Read more here). We have already learned one: __init__. Let’s implement the __repr__ method to make our class display something pretty.

class Hurricane:

    def __init__(self, name, category, lon):
        self.name = name.upper()
        self.category = int(category)

        if lon > 180 or lon < -180:
            raise ValueError(f'Invalid lon {lon}')
        self.lon = lon

    def __repr__(self):
        return f"<Hurricane {self.name} (cat {self.category})>"

    def is_dangerous(self):
        return self.category > 1
f = Hurricane('florence', 4, -46)
f