Referecing, Advanced Functions and Data structures

Contents

Referecing, Advanced Functions and Data structures#

Python Programming for Engineers#

Tel-Aviv University / 0509-1820 / Fall 2025-2026#

Agenda#

Lists

  • List comprehensions

Assignments

  • References
  • Mutable vs. immutable

Functions

  • Different kinds of functions: Methods, Built-ins (self-learning)
  • Scopes
  • Global vs. local variables
  • Different ways to retrieve a value from a function

New data structures

  • tuple
  • Dictionary (dict)

List comprehension#

  • Pythonic way of creating lists.

  • Combining lists and for loops.

  • Syntax:

[expression for item in list]
[expression for item in list if condition]

Examples#

[1 for _ in range(10)]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
[i for i in range(7) if (i%3==0 or i%4==0)]
[0, 3, 4, 6]
l=["spaceship", "says:", "keep", "summer", "safe!"]
print([a.upper() if a[0]!='s'  else a for a in l])
['spaceship', 'says:', 'KEEP', 'summer', 'safe!']
print([i*2 if i%3==0 else i for i in range(10) if (i%2==0 or i%3==0)])
[0, 2, 6, 4, 12, 8, 18]

Python references#

  • Assigning a variable in Python creates:

    • An object storing the value

    • A name variable holding a reference to the object.

alt_text

  • Assigning a new variable to an existing variable copies the reference

alt_text

  • Name variables can be redirected to point new objects

alt_text

List references#

alt_text

alt_text

alt_text

alt_text

Example#

a=[1,2,3]
b=a
a.append(4)

print(a)
print(b)
[1, 2, 3, 4]
[1, 2, 3, 4]

alt_text

Differnet kinds of functions#

Methods:#

Functions which are members of specific types (remember list’s methods from previous recitation)#

  • Call by: variable_name.method_name(arguments)

  • Examples:

    “method-like” syntax

    “function-like” syntax

    my_string.upper()

    str.upper(my_string)

    my_list.append(7)

    list.append(my_list, 7)

    • method-like == Object-Oriented. In a few weeks from now…

  • Write variable_name. Then type tab to get the list of variable members (press tab to choose one)

Built-in functions (colored in PyCharm).#

  • e.g., abs len

Scopes#

Scope refers to which parts of your program can see or use a variable.#

When python executes a function it creates a new scope for it:#

  • Defined parameters are local

  • They are defined in the scope and then “disappear”

  • Calling statement “sees” only the results

Global vs. local variables#

  • Variables declared inside a function are local

  • Variables declared outside a function are global

  • global variables can be accesssed from inside function (even if were not passed as arguments)

    • However, assiging the variable a new value “hides” the global variable for the entire scope of the function

    • This behaviour can be overridden with the global statement (extra - not in course material this year)

# variable s is not defined

def f():
    print(s)    
    
f()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 6
      2 
      3 def f():
      4     print(s)
      5 
----> 6 f()

Cell In[7], line 4, in f()
      3 def f():
----> 4     print(s)

NameError: name 's' is not defined
# variable s is defined only outside
def f():
    print(s)    
    
s = 'Rick'
f()
Rick
i = "Rick" 
def foo(): 
    i = "Morty" 
    print(i)

print(i)
foo() 
Rick
Morty
def foo(): 
    i = "Morty" 
    print(i)


i = "Rick" 
print(i)
foo()
Rick
Morty
# variable s is defined also in but only after using s by the print function
def f():
    print(s)
    s = 'Morty'
    print(s)
    
s = 'Rick'
f()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Cell In[11], line 8
      4     s = 'Morty'
      5     print(s)
      6 
      7 s = 'Rick'
----> 8 f()

Cell In[11], line 3, in f()
      2 def f():
----> 3     print(s)
      4     s = 'Morty'
      5     print(s)

UnboundLocalError: cannot access local variable 's' where it is not associated with a value

Passing (retrieving) and arguments to (from) functions#

  • The caller specifies arguments (values) for the function call.

  • These arguments are assigned to the variables, with which the function works.

  • As we saw: assigning a value val to a variable named x makes x a reference to val.

image-4.png

Whether or not the function can change these values depends on whether their type is mutable.

Examples#

Immutable example#

def power_for_num(n, pow):
    n = n**pow 
    return n 
n = 3
print(n)
result = power_for_num(n,4) 
print(result)
print(n)
3
81
3

The function stored the value in the variable n, but the global variable didn’t change.

Mutable examples#

def increment(lst):
    for i in range(len(lst)):
        lst[i] = lst[i]+1
        
list1 = [1,3,7]
increment(list1)
print(list1) 
[2, 4, 8]

list1 was mutated inside the body of increment(lst).
Such a change can occur only for mutable objects.

def increment_new_list(lst):
    new_lst = lst[:]
    for i in range(len(lst)):
        new_lst[i] = new_lst[i]+1
    return new_lst
list1 = [1,3,7]
print(list1)
list2 = increment_new_list(list1)
print(list2) 
[1, 3, 7]
[2, 4, 8]

Mutable arguments: clear a list#

def nullify(lst):
    lst = []

list1 = [1,3,7]
nullify(list1)    
print(list1)
[1, 3, 7]
def nullify2(lst):
    lst.clear()

list1 = [1,3,7]
nullify2(list1)    
print(list1)
[]
list1 = [1,3,7]

nullify2(list(list1))
print(list1)
nullify2(list1[:])
print(list1)
[1, 3, 7]
[1, 3, 7]

Summary: how to retrieve information from a function?#

  • Return a value(s)

  • Change a mutable argument

  • Global keyword (out of scope)

New data structures#

Tuples#

  • A tuple is just like a list, only it is immutable.

  • Syntax: parentheses instead brackets!

r = ('Rick', 'Sanchez', 'C', 137)
print(r)
('Rick', 'Sanchez', 'C', 137)
print(r[0])
print(r[-1])
print(r[1:3])
Rick
137
('Sanchez', 'C')
r[0]='Morty'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[21], line 1
----> 1 r[0]='Morty'

TypeError: 'tuple' object does not support item assignment

Dictionaries#

  • A dictionary maps keys to values.

  • Keys aren’t necessarily of the same type, and so are values.

alt_text

# Define a new dictionary
rick_dict = {'Series': 137, 'Comic-book': 132}
print(rick_dict)
{'Series': 137, 'Comic-book': 132}

Add (or replace) a pair:#

rick_dict['Series'] = 137
print(rick_dict)
{'Series': 137, 'Comic-book': 132}

What is the values of ricks?#

print(rick_dict['Series'])
137
print(rick_dict['Comic-book'])
132
print(rick_dict['comic-book'])
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[26], line 1
----> 1 print(rick_dict['comic-book'])

KeyError: 'comic-book'

Check if a key is in dict:#

'Series' in rick_dict

What can be used as a key?#

rick_dict[["evil", "tiny"]] = 99
print(rick_dict)

Immutable types only!#

  • Strings, numbers, booleans, tuples…

  • If a tuple contains ANY mutable objects (either directly or indirectly), it cannot be used as a key

rick_dict[("cool", "evil")] = 999
print(rick_dict)
rick_dict[("cool", "evil", ["tiny"])] = 9

Common Dictionary methods#

Method\(~~~~~~~~~~~~~~~\)

Description

dict.get(k, [d])

Returns dict[k] if dict has the key k, otherwise d (default: d=None)

k in dict

Returns True if dict has a key k, otherwise False (The same as with lists, strings, tuples).

dict.pop(k,[d])

Removes from dict the specified (key, value) pair with key k, and returns its value. If k is not in dict, returns d. If d is not given and k is not in dict, an error is raised

dict.keys()

Iterable view of dict’s keys in arbitrary order

dict.values()

Iterable view of dict’s values in arbitrary order

dict.items()

Iterable view of dict’s (key, value) pairs in arbitrary order

dict.update([other])

Updates dict with the (key, value) pairs from other (another dictionary object or an iterable of (key, value) pairs). Returns None.

dict.copy()

Creates a shallow copy of dict (constructs a new dictionary, and inserts references to the original keys/values)

Examples for Dictionary usage#

Example 1: sparse matrix#

  • In this matrix there are only 3 non-zero entries.

    • Saving all zeros in a list is a waste of space.

  • As a dict: A key is a tuple indicating the row-column pair (i,j) whereas a value is the non-zero entry.

matrix = [[0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 2, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 3, 0]] # Long!
print(matrix[1][2])
print(matrix[0][3])
matrix = {(0, 3): 1, (2, 1): 2, (4, 3): 3} # Shorter!
print(matrix.get((1,2),0))
print(matrix.get((0,3),0))

Example 2: pangrams#

A pangram is a sentence that contains each letter of the English alphabet at least once.#

Write a function that determines if a sentence is a pangram:#

  • Using list

  • Using dict

Implementation using list#

def is_pangram_list(s):
    l = [False]*26
    for char in s:
        if char.isalpha():
            l[ord(char.lower())-97] = True 
    return all(l)

Implementation using dict#

def is_pangram_dict(s):
    d = {}
    for char in s:
        if char.isalpha():
            d[char.lower()] = True
    return len(d) == 26
s = "The quick brown fox jumps over the lazy dog"
print(is_pangram_dict(s))
print(is_pangram_list(s))
s = "The quick brown fox jumps over the lazy Dog"
print(is_pangram_dict(s))
print(is_pangram_list(s))
s = "The quick brown fox jumps over the lazy Rick"
print(is_pangram_dict(s))
print(is_pangram_list(s))

Is it better to use here dict instead of list?#

Which implementation is more intutive (remember, readability)?#

  • Not sure…

Which implementation prone to errors?#

  • Probably the list…

Which implementation is more efficient?#

  • Because the English alphabet has only 26 letters, we can also solve this with a list (initialized as: [False] 26).

  • However, if the alphabet had 10,000 letters dict would certainly be better.

Example 3: two dice#

Task#

  • Rolling two backgammon dice yield 36 possible combinations.

  • We would like to know the probability to get each sum of the two dice

    • In other words, the fraction of combinations that sums to a certain result.

  • Therefore:

    1. Collect all the combinations for every possible sum.

    2. Find the probability of getting each sum.

Solution#

Create the following data structure:

  • Each combination of two dice is a tuple.

  • All combinations are held in a dict, where the key is the sum of the combinations.

  • Each value in the dictionary is a list of tuples – that is, all combinations that sum to key.

General outline#

  1. Generate dictionary.

  2. For each key: print key, length of its list.

Pseudo-code#

rolls = {}
Loop with d1 from 1 to 6:
\(~~~~\)Loop with d2 from 1 to 6:
\(~~~~\)\(~~~~\)key ← d1 + d2
\(~~~~\)\(~~~~\)newTuple ← (d1, d2)
\(~~~~\)\(~~~~\)oldList ← dictionary value for key
\(~~~~\)\(~~~~\)newList ← oldList + newTuple
\(~~~~\)\(~~~~\)update: dictionary[key] ← newList

(2) implement pseudo-code (two dice)#

rolls = {}
for dice1 in range(1, 7): # first dice
    for dice2 in range(1, 7): # second dice
        dices = (dice1, dice2)
        dices_sum = dice1 + dice2
        combinations = rolls.get(dices_sum, [])
        combinations.append(dices)
        rolls[dices_sum] = combinations

for dices_sum, combinations in rolls.items():
    print(dices_sum,"\t",len(combinations)/36)

Note that we do not actually need the tuples themselves.#

  • Can we improve this code?

  • Count number of combination instead of store them in lists

    • Memory efficient

rolls = {}
for dice1 in range(1, 7): 
    for dice2 in range(1, 7): 
        dices_sum = dice1 + dice2
        rolls[dices_sum] = rolls.get(dices_sum,0)+1
        
for dices_sum, n_combinations in rolls.items():
    print(dices_sum,"\t",n_combinations/36)

Self learning#

List comprehensions#

What will these list comprehensions return?

[i if i in "aeiou" else "q" for i in "burrito"]
[i-1 if i%2==0 else i+1 for i in range(10) if i%3 == 0]

Summarizing example of list comprehension. Make sure you understand each part of it!#

l = ['Boom!', 'Big', 'reveal!!','I', 'turned', 'myself', 'into', 'a', 'pickle!']
[a if len(a)> 5 else b for a in l for b in a if "!" in b]

Implement the following in one line:

  1. Given a string s, generate a list of all the capital letters in s.

  2. 7-Boom! Create a list of all the numbers from 1 to 100, in which every number divisible by 7 or containing the digit 7 is replaced by the string “Boom!”.

More function examples#

Exercise 1#

implement the function students_with_max_grade:

  • Input: a list of grades, and a list of student names corresponding to the grades.

  • Output: a list of students with maximum grade

# Solution
def students_with_max_grade(grades, names):
    '''return a list of the students with the maximal grade'''
    max_grade = max(grades)
    max_grade_students = []
    for i in range(len(names)):
        if grades[i] == max_grade:
            max_grade_students.append(names[i])
    return max_grade_students

grades = [100, 73, 71, 37]
names = ["Rick", "Morty", "Summer", "Jerry"]
print(students_with_max_grade(grades, names))

Exercise 2#

implement the function give_factor:

  • Input: a list of grades, a positive int representing a factor of y.

  • Goal: give non-failed grades a factor of y

  • No return statement!

Hint: Change the list inplace.

# Solution
def give_factor(grades, factor):
    for i in range(len(grades)):
        if grades[i] >= 60:
            grades[i] += factor
            if grades[i] > 100: # Can also be  grades[i] > min(grade[i]+factor,100)
                grades[i] = 100
                
grades = [100, 73, 71, 37]
print(grades)
give_factor(grades, 5)
print(grades)

Example 3#

  • What if we want to apply different factors?

  • Some factors are non linear, other factors may be applied to specific range of grades. For these reason, give_factor should be as general as possible, and the factor should be passed as a function

# Squared factor. e.g, 64->80, 81->90. 
def sqrt_factor(grade):
    if grade >= 60:
        return int(10*(grade**0.5))
    return grade

# Evil factor: Increases the average but none of the students is happy: Students who passes get the same grade. Students who failed get a higher grade, but they still fail.
def evil_factor(grade):
    if grade < 59:
        return 59
    return grade

def give_factor(grades, factor_func):
    for i in range(len(grades)):
        grades[i] = min(factor_func(grades[i]), 100)
    
grades = [100, 73, 71, 37]
give_factor(grades, sqrt_factor)
print(grades)
give_factor(grades, evil_factor)
print(grades)
  • give_factor can also be called with a lambda function, which is an inline definition of a function

  • lambda function an inline implementation of a function w/o declaring it using the def statement as we did so far.

grades = [100, 73, 71, 37]
give_factor(grades, lambda x: x+2)
print(grades)
give_factor(grades, lambda x: x if x<60 else x+3)
print(grades)

Dictionaries#

Exercise 1#

implement the function count_grades:

  • Input: A dictionary grades mapping from student name to grade.

  • Goal: A dictionary counting how many student got each grade.

Example: count_grades({"Rick": 100, "Morty": 62, "Jerry": 62}) will return {100: 1, 62: 2}

# Solution 1 
def count_grades(grades):
    grades_counter = {}
    for grade in grades.values():
        if grade in grades_counter:
            grades_counter[grade] += 1
        else:
            grades_counter[grade] = 1

# Solution 2 (better with get!)        
def count_grades(grades):
    grades_counter = {}
    for grade in grades.values():
        grades_counter[grade] = grades_counter.get(grade, 0) + 1

Exercise 2#

implement the function create_sparse_matrix:

  • Input: A matrix of numbers implemented by a list of lists.

  • Goal: A dictionary representing the sparse matrix (As shown above).

# Solution
def create_sparse_matrix(matrix):
    matrix_dict = {}
    for i in range(len(matrix)):
        for j in range(len(matrix[i])):
            if matrix[i][j] != 0:
                matrix_dict[(i,j)] = matrix[i][j]
    return matrix_dict