Python for AI 103

Acknowledgement: This post is based mainly on Dr. Quang-Vinh Dinh’s presentation and slides and partly on on TA, STA, and peers’ responses from the course, AIO2025, offered by AI VIETNAM.

Relevance: The audience of this lecture were those who have read Python for AI 102, had some coding experience, and known a bit of AI knowledge. Absolute beginners are encouraged to read the material and repeatedly practice coding without AI assistance to strengthen their coding and code-reading skills. Note that only necessary Python knowledge is introduced here.

Think about anything you learn as a game. A game always has rules. We just need to follow the rules to solves problems and conquer the game. When learning programming, don’t trust anyone or any book. Only be convinced that a program is correct once you have already tried it yourself, seen the expected result, and explore the reasoning behind the answer through questioning.
-Dr. Quang-Vinh Dinh

In today’s post, we’ll delve into lists. We won’t approach it like computer scientists who know how to integrate it into embedded systems and make physical devices perform specific tasks. Instead, we’ll take a programming-focused approach.


:pushpin: Pro tip: When learning a new programming concept, it’s best practice to read the documentation and experiment with different examples to gain a deeper understanding.

Data Structure

Data structures were orginally invented to help computers handle data faster. Realistically, speed has become less of a concern due to regular hardware upgrades.

A data struture is a format used to organize and store data for efficient access and management. It often mirrors real-life situations, for instance, organizing words in a dictionary, managing a customer queue, or building a family tree.

There are two types of data structures and they are arranged in order of their usage frequency from most to least commonly used. Lists can be used to custom user-defined data structures.

Built-in User-defined
List Stack
Dictionary Queue
Tuple Tree
Set Graph
  Linked List

1D List

list_name = [element_1, ..., element_n]

A list is a container of elements. As mentioned in the previous post, a list can contain different types of data, but we stick with only one data type per list in practice—especially numbers—since computers internally works with numerical data.

list_0 = [] # an empty list
list_00 = list() # an empty list
list_1 = [7, -5, 8]
list_2 = [5, 4.7, -2, "hello", 0, True, -1000.1, 5, 5, 5]
list_3 = ["aivietnam", [2, -3, 0, 5]]

Some basic rules for lists are:

  • All elements are surrounded by a pair of brackets.
  • Every two consecutive elements are separated by a comma.
  • As soon as a list is created, so are its indices.
    • :arrow_right: Forward index: starts at $0$ and increases by $1$.
    • :arrow_left: Backward index: starts at $-1$ and decreases by $1$.
    • list_name[index] accesses an element and index must be less than the number of elements in a list.

We mainly use forward index, and sometimes use $-1$ from backward index to retrieve the last element.

list_1 = [7, -5, 8] 

# test
list_1[0]
>>> 7

list_1[-1]
>>> 8

list_1[9]
>>> IndexError

Operators

We can manipulate lists by performing various operations on them.

Updating an Element

Lists are mutable, meaning their content can be changed after creation. Mutability and immutability are coming soon in this post.

list_1 = [7, -5, 8] 
list_0 = []

# test
list_1[2] = 10000
list_1
>>> [7, -5, 10000]

list_0[1] = 5
list_0
>>> IndexError

Slicing

Similar to the range(start, stop, step) function, the slicing syntax of list is list[start:end:step].

  • start, end, and step are indices. Their values are all optional.
  • start is $0$ and step is $1$ by default.
  • If end is not specified, slicing takes all elements till the last element of one side depending on step.

Slicing creates an entirely new list—changing its elements’ value does not affect the original list and vice versa.

a_list = [3, 4, 5, 6, 7, 8, 9, 10]

# test
a_list[:3]
>>> [3, 4, 5]

a_list[6:]
>>> [9, 10]

a_list[0:1000:3]
>>> [4, 7, 10]

a_list[::-1]
>>> [10, 9, 8, 7, 6, 5, 4, 3]

a_list[3:1]
>>> [] 
Practice Exercises
Given the list animal = ['e', 'l', 'e', 'p', 'h', 'a', 'n', 't']. Use slicing in different ways if possible to answer the following.
  1. Get the first four letters.
  2. Get the last three letters.
  3. Get the every second letter starting from index 1.
  4. Get the all letters except the first and last.
  5. Get the middle three letters.
  6. Get an empty list.
  7. Reverse the letters.
  8. Get the letters h, p, e .
  9. What does animal[1:-1:2] return?


$+$ and $*$ Operators

The $+$ operator is to concatenate lists. The $*$ operator is to make copies of a list and concatenate them together. The original list remains unchanged unless it is reassigned to the result of the addition or multiplication.

list_1 = [7, -5, 8]
list_2 = [5, 4.7, -2, "hello", 0, True, -1000.1]

# test
addition = list_1 + list_2
addition
>>> [7, -5, 8, 5, 4.7, -2, "hello", 0, True, -1000.1, 5, 5, 5]

multiplication = list_1 * 3
multiplication
>>> [7, -5, 8, 7, -5, 8, 7, -5, 8]

The $*$ operator can be used when we want a list to be initialized with some default value. For example, a teacher is making a file to input students’ quiz grades later. They can do quizzes = [-1] * 50 to avoid typing one number too many times and know which grades have been updated.

Mutable and Immutable

Practice Exercises Point out the differences between the functions below. Is the original list updated in both? Explain why or why not. How is it related to immutable and mutable here? What are pros and cons of each? In which cases should we use them?
Immutable Mutable

def square(data):
    result = []
    for value in data:
        result.append(value * value)

    return result

# test
data = [6, 5, 7, 1, 9, 2]
print(data)

data_s = square(data)
print(data_s)


def square(data):
    length = len(data)
    for i in length:
        value = data[i] * data[i]
        data[i] = value

# test
data = [6, 5, 7, 1, 9, 2]
print(data)

data_s = square(data)
print(data_s)

Pros:
  • Save some future's troubles.
  • Can be easier to design different parts of the program.
Pros:
  • Save memory.
Cons:
  • Cost more memory.
Cons:
  • Potentially contain a critical bug that needs handling more elegantly.


Methods

list_name.method_name()

Classes which we’ll learn in Python for AI 105 can be used to implement data structures. A class of list includes certain methods, so lists can access all of them. Some methods update a list when applied, which avoids the need to assign a new variable and thus saves memory.

We’ll use a_list = [4, 5, 6, 7, 4, 7] for examples below.

Methods Purposes Returned Value List Updated Examples
append() Add a given value to the end None :white_check_mark: a_list.append(10)
a_list
>>> [4, 5, 6, 7, 4, 7, 10]
insert() Add a given value to the position of a given index None :white_check_mark: a_list.insert(2,10)
a_list
>>> [4, 5, 10, 6, 7, 4, 7]
extend() Concatenate a given list to the original None :white_check_mark: a_list.extend([10, 8])
a_list
>>> [4, 5, 6, 7, 4, 7, 10, 8]
pop() Delete the last element by default or delete by index Value :white_check_mark: a_list.pop(1)
>>> 5
a_list
>>> [4, 6, 7, 4, 7]
remove() Delete the first occurence of a given value None :white_check_mark: a_list.remove(7)
a_list
>>> [4, 5, 6, 4, 7]
del Delete a given list of elements N/A :white_check_mark: del a_list[1:4]
a_list
>>> [4, 7, 7]
clear() Delete all elements None :white_check_mark: a_list.clear()
a_list
>>> []
index() Return the index of the first occurence of a given value Index :x: a_list.index(7) >>> 3
a_list
>>> [4, 5, 6, 7, 4, 7]
reverse() Reverse elements None :white_check_mark: a_list.reverse()
a_list
>>> [7, 4, 7, 6, 5, 4]
sort() Arrange elements in an increasing order by default None :white_check_mark: a_list.sort()
a_list
>>> [4, 4, 5, 6, 7, 7]
count() Return the number of a given value Number of times an element appears :x: a_list.count(7)
>>> 2
a_list
`  »> [4, 5, 6, 7, 4, 7]`
copy() Make a copy of the list New list that has the same value as the original :x: a_list.copy()
>>> [4, 5, 6, 7, 4, 7]
a_list
>>> [4, 5, 6, 7, 4, 7]


:warning: list_name.copy() and direct assigment of one list to another operate differently behind the scenes.

Tasks copy() Method Assignment (=)
Code

original_data = [1, 2]

copied_data = original_data.copy()
copied_data[0] = 9

print(original_data)
print(copied_data)


original_data = [1, 2]

copied_data = original_data
copied_data[0] = 9

print(original_data)
print(copied_data)

Rendered output

[1, 2]
[9, 2]


[9, 2]
[9, 2]

Explanation
  • It's like two bags, one brand name (original_data) and one replica (copied_data).
  • They look the same but how one is used has no effect on the other.
  • Technically, copy() creates a new list object which lives in a different memory address from the existing list.
  • It's like the same person with a real name (original_data) and a nick name (copied_data).
  • copied_data does whatever original_data does and vice versa.
  • Technically, assignment creates a reference to the existing variable, allowing both to point to the same list object in memory.


:pushpin: Pro tip: For newbies, try to create built-in methods.

In fact, we are able to write our own functions that replicate the behavior of these methods using only basic concepts we have learned so far.

Practice Exercises
Exercise 1: I write the funciton below to insert a given value to a list at a given index. Will my code work as expected? Explain why or why not. If it doesn't, suggest at least one solution to fix it.

def my_insert(data, index, value):
    length = len(data)
    result = []
    for i in range(length):
        if i == index:
            data.append(value)
            data.append(data[i])

        data.append(data[i])

    return result

Exercise 2: Write different functions that act like insert(), at least one with returning a value and one without.
Exercise 3: Write functions for other methods if possible.
Exercise 4: remove() only deletes the first occurence of a given value. Can you write a function that deletes all the occurences of a given value? If yes, write the function. Or else, explain why not.
Exercise 4: What is the output of the following?

data1 =[1, 2, 3]
data2 = data1
data3 = data1.copy()

data1[0] = 9
print(data1[0], data2[0], data3[0])


Built-in Functions

Functions Purposes Returned Value List Updated Examples
len() Return the number of elements Quantity :x: len(a_list)
>>> 6
min() Return the minimum element Quantity :x: min(a_list)
>>> 4
max() Return the maximum element Quantity :x: max(a_list)
>>> 7
sorted() Arrange elements in an ascending order by default New List :x: sorted_a_list = sorted(a_list)
sorted_a_list
>>> [4, 4, 5, 6, 7, 7]
sum() Return the sum of all elements Quantity :x: sum(a_list)
>>> 33
reversed() Reverse elements A reverse iterator object :x:  
enumerate() Return a sequence of tuples of matching index and value An enumerate object :x:  
zip() Take two lists and return an iterator of tuples of values with matching index A zip object :x:  


enumerate() replaces a regular for loop to access indices and values in a list, speeding up the coding process.

data_0 = [4, 5, 6]

# print index and value in pairs using enumerate()
for index, value in enumerate(data_0):
    print(index, value)

# The rendered output is
0 4
1 5
2 6

zip() acts like a zipper that interlocks two rows of teeth. Difference in length of the rows doesn’t matter; it can zip to the end of the shorter one. We use this method when working with data in pairs.

data_1 = [1, 2, 3]
data_2 = ['john', 'mary', 'peter']

# print in pairs using zip()
for position, name in zip(data_1, data_2):
    print(v1, v2)

# The rendered output is
1 john
2 mary
3 peter
Practice Exercises
Exercise 1: Write functions for other built-in functions if possible.
Exercise 2: Write a function that sums up even and odd numbers.
Exercise 3: Write a function that sums up elements with even and odd indices.


List Comprehension

list_name = [condition_to_branch_x for x in data condition_to_filter_x]

List comprehension is favored among programmers, allowing us to write a simple for loop in only one line. A typical list comprehension has three parts as above.

:question: You may ask: Do I need all parts for list comprehension? How am I reading this, from left to right?

A short answer for the first question is no; we’ll discuss the details in a moment. For the second question, well yeah if we read it like an English text block. But, this is Python language; we read list comprehension in the following order.

  1. Looping for x in data.
  2. Filtering condition_to_filter_x.
  3. Routing condition_to_branch_x.

Imagine a company is hiring summer interns using a list of job-seekers. They need to evaluate all of them one by one. They can either accept all the candidates or filter some out.

(Optional) condition_to_filter

if x_filter_condition

The filter condition can only have if. Here the company decides to be selective, setting up an interview for each applicant. If someone passes the interview, they proceed with the internship. Otherwise, they’re out.

(Required) condition_to_branch_x

[if_value if x_if_condition else else_value for x in data]

[if_value if x_if_condition else elif_value if x_elif_condition else else_value for x in data]

The branch condition must be either if-else or if-elif-else. For the latter, it’s written as nested if-else. A syntax error will be raised if an else is missing.

Now, for those joining the internship of the company, the supervisor must assign them jobs based on their qualifications. This makes it logical to have if-else or if-elif-else in this part. Besides, without an else, it means an incoming intern who has got hired must still be qualified for something else to be hired again. What about those who cannot meet that “something else”? That makes no sense.

Practice Exercises
Exercise 1: What is the output of this program? Give your reasoning.

data = [1, 2]

for x in data:
    x = 20

print(f"data is {data}")

Exercise 2: What is the output for a_list = [4, 5, 6, 7, 4, 7]?
  1. a_list.sort(rever = True)
  2. sorted(a_list, reverse = True)
Exercise 3: Write a for loop that squares all elements of a list. Also write it with list comprehension.
Exercise 4: Write the following activation functions for lists using list comprehension. Write helper functions when needed.
  1. Sigmoid function
  2. ReLU function
Exercise 5: What is the output of print(data_a) for each list comprehension line below given data = [1, 5, -4, 2, 3, -2, 8, 7]? Explain your answer.

data_a = [x if x > 0 else 0 for x in data]
data_a = [x if x > 0 for x in data]
data_a = [x for x in data if x > 0]
data_a = [x for x in data if x > 0 else 0]
data_a = [x if x%2==0 else 0 for x in data if x > 4]


2D List

Real-world data extensively need more-than-one-dimension structure/matrix to represent it. For instance, color image data (at least 3D), gray scale image data (2D), and text data (2D). Later on, we’ll use a package called NumPy for these scenarios but use list for now.

We’ll focus on two dimensions here. To describe a 2D matrix, we can use a nested list—a list where each element is a list of the same size. Take data_2d = [[1, 2, 3], [4, 5, 6]].

Plain Language Python Language
data_2d is a box data_2d points to a list object
the box holds 2 name tags a list object contains two pointers
the first tag says “look over here”, which directs to a baby box named [1, 2, 3] the element of index 0 points to another list object with value [1, 2, 3]
the second tag says “look over there”, which directs to another baby box named [4, 5, 6] the element of index 1 points to another list object with value [4, 5, 6]


Since each element of a nested 2D list is a list itself, we can access the inner elements using the same indexing syntax as with regular lists. For example, data[1][2] = 6.

A nested 2D list can be thought as a matrix. Each outter element is a row and the columns are formed by items of the same index in each inner list. So, the number of rows is the number of outer elements while the number of columns is the size of the inner list. In this case, this matrix has 2 rows and 3 columns.

data_2d = [[1, 2, 3], [4, 5, 6]]

num_rows = len(data_2d)
num_cols = len(data_2d[0])

# iterate through a nested list using a nested loop and print the matrix
for i in range(num_rows):
    for j in range(num_cols):
        # print each row
        print(data_2d[i][j], end = ' ')

    print()

'''
The rendered output is
1 2 3
4 5 6
'''
Practice Exercises
Exercise 1: Write a function that prints a 2D matrix given a list, the number of rows, and the number of columns with and without returning a value.
Exercise 2: Use exercise 1 to write a function that prints a 2D matrix whose elements are square of elements of a nested list with or without returning a value.
  1. a_list.sort(reverse = True)
  2. sorted(a_list, reverse = True)
Exercise 3: Write a function that performs Hadamard Product (Element-wise Multiplication)
Exercise 4: Use exercise 1, what is the output?

width = 2
height = 3
matrix = [[0, 0], [0, 0], [0, 0]]

for i in range(height):
    for j in range(width):
        matrix[i][j] = i * width + j

print_matrix(matrix)


width = 3
height = 3
matrix = [[1, 1, 1], [1, 0, 0], [1, 0, 0]]

for i in range(1, height):
    for j in range(1, width):
        matrix[i][j] = matri[i-1][j] + matrix[i][j-1] + matrix[i-1][j - 1]
        
print_matrix(matrix)


Algorithm and Case Study

Case Study: List and Integral
Problem statement: Given a matrix, compute the sum and average of elements of a submatrix. Take a 1D matrix for convenience.
Breakdown:
  • $f(x)$ is the value of the element at index $x$.
  • $F(x)$ is the sum of a matrix---all elements from index $0$ to $x$.
  • $A$ is the sum of the submatrix---all the elements from one position to another.
Idea: We can use the idea of integral instead of accummulation pattern.
  1. Let's go over integral method in Calculus first.
    • $F(a) = \int_{-\infty}^{a} f(x)dx = \sum_{\Delta{x} \to 0} f(x) \Delta{x}$: summation of rectangle areas
    • $F(b) = \int_{-\infty}^{b} f(x)dx = \sum_{\Delta{x} \to 0} f(x) \Delta{x}$
    • $A = \int_{a}^{b} f(x)dx = F(b) - F(a)$.
    • As $c$ is very close to $a$, $F(a) = \int_{-\infty}^{a} f(x)dx + f(c)dx = F(a) + f(c)dx$.
  2. Applied to our problem, we have
    • $F(x) = F(x-1) + f(x)$
    • $A = \sum_{a}^{b} f(x) = F(b) - F(a-1)$: summation from index $a$ to $b$.

    Assume the calculation of $F(x) $ is provided, we just need to know the values of two positions to complete the task.
  3. DEMONSTRATION AND EXAMPLE
  4. The cost to perform this method is low as we can avoid running through the whole submatrix and do accumulation.
This method is called Integral Image and can be found in image data problems like Face Detection in CVPR.
Practice Exercise
Exercise 1: Write a function that mimics index() using enumerate().
This exercise illustrates Linear Searching Algorithm on List, an algorithm for finding an element that equals some given value by searching through elements of a list one by one.
Exercise 2: Write a function that mimics sort() using min(), remove(), append().
Exercise 3: Provide an idea for the case study with a 2D matrix.

:speech_balloon: FAQ

Here are some good questions asked during class. The answers to these questions came from either some admin, TA, STA, or members of AIO2025.

Questions Answers
What is an embedded system? No. Python reads code top-down.
Can I write 2 functions A and B where A calls B and B calls A? Why does it give an error when I refer to one before defining it? Yes. Such a situation is called mutual recursion. Python doesn’t execute function definitions which consist of function references. It only starts evaluating when a function is called.
A resource for mutable and immutable. https://www.geeksforgeeks.org/mutable-vs-immutable-objects-in-python/
A resource for pass by reference and pass by value https://www.tutorialspoint.com/python/python_functions.htm
How do I tell when two variables point to the same object in memory? You either need to understand what’s going on very well or check with the id() function. Each object has one and only one identity.
Can I use list comprehension to create a completely new list? Yes. It all depends on how you choose your values inside the branch condition.
Visualization for Sorted List https://www.cs.usfca.edu/~galles/visualization/Search.html
Are integral image and box filtering similar? Yes. Box filtering is in graphics.



    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • Python for AI 104
  • Python for AI 101
  • Math for AI 101
  • Python for AI 106
  • Python for AI 103 Warm-up