Session 2

  • Lists and tuples
  • Loops
  • Dictionaries and comprehensions
  • Functions

Learning outcomes

Learning outcomes: Lists and tuples

You’ll be able to:

  • organize data in lists
  • organize data in tuples
  • access elements by index and extract parts of a list or tuple with slicing
  • use functions and methods to manipulate lists: len, append, insert, remove, pop, sort, …
  • find values in a list: in, index, count
  • explain the difference between mutable and immutable objects
  • unpack lists and tuples into variables

Learning outcomes: Loops

You’ll be able to:

  • Use loops to execute a set of instructions multiple times
  • Use loops to iterate over data structures

Learning outcomes: Dictionaries and comprehensions

You’ll be able to:

  • Organize data in dictionaries
  • Create lists and dicts concisely with comprehensions

Learning outcomes: Functions

You’ll be able to:

  • Write functions to reuse code, with arguments and return values (including several values at once)
  • Distinguish local from global variables and explain when global is needed
  • Predict when a function changes a list passed to it
  • Treat functions as values: assign them to variables, pass them to other functions, write short lambdas
  • Use default arguments, *args and **kwargs
  • Annotate argument and return types

Unit 4

Lists and tuples

Basic data structures: list

So far we’ve stored only single values in variables.

Python’s list allows us to store multiple values.

Create empty list

We create a list with the list() function or simply [].

l1 = list()
l2 = []

Create list with content

We can create a list initialized with some elements.

heart_rates = [72, 78, 75, 82, 79]  # patient pulse readings (bpm)
temperatures = [36.8, 37.2, 37.0, 38.1, 37.5]  # patient body temperatures (Celsius)
conditions = ["diabetes", "hypertension", "pneumonia"]
lst_mixed = [72, 36.8, "diabetes"]

Size of the list

heart_rates = [72, 78, 75, 82, 79]  # patient pulse readings (bpm)
len(heart_rates)  # this function gives the size of the list
                  # and many other data structures -> 5

Adding elements

Like strings, lists have several methods that help with their interaction.

temperatures = [36.8, 37.2, 37.0]
temperatures.append(38.1)  # add new patient reading
temperatures += [37.5]  # add another reading
print(temperatures)  # -> [36.8, 37.2, 37.0, 38.1, 37.5]
temperatures += [37.5]  # add another reading

What is +=?

It looks like a shortcut for

temperatures = temperatures + [37.5]

Lists support the + operator: the list on the right is concatenated to the list on the left.

Almost the same, but not quite: += changes the list in place, while + creates a new list. We’ll see why this matters in Mutable vs Immutable.

We can also add elements at specific positions with the .insert method.

# insert normal reference temperature at the beginning
temperatures.insert(0, 37.0)

Getting an element

List indeces are 0-based.

l = [1,2,3,4,5,6]
l[0]  # gives the 1st element -> 1
l[1]  # gives the 2nd element -> 2
l[-1] # gives the last element -> 6
l[-2] # gives the second last element -> 5

Slicing

We may get a slice of the list.

l = [1,2,3,4,5,6]
l[0:3]  # returns a list with all elements
        # from the 1st to the 3rd elements
        # -> [1,2,3]

The second index is not included in the slice.

Slicing rule

  • list[A:B] or list[A:B:C]
  • A is the starting index of the slice
    • if not supplied, it’s assumed to be the beginning
  • B is the last index of the slice, but is not included
    • if not supplied, it’s assumed to be the end
  • C is the jump between consecutive elements
    • if not supplied, it’s assumed to be 1
    • if it is negative, we get elements in the reverse order

Slicing examples

l = [1,2,3,4,5,6]
l[0:3]  # returns a list with all elements starting 
        # on index 0 and ending on 2 -> [1,2,3]
l[0:1]  # returns a list with just the 1st element -> [1]
l[1:1]  # returns an empty list -> []
l[:]    # returns all elements of the list -> [1,2,3,4,5,6]
l[:-2]  # returns all elements except the last 2 -> [1,2,3,4]

More Slicing

l = [1,2,3,4,5,6]
l[-3:-1] # all elements starting on the 3rd last position
         # and finishing on the 2nd last -> [4,5]
l[-3:]   # all elements from the 3rd last onwards -> [4,5,6]
l[::2]   # every other element, starting on index 0 -> [1,3,5]
l[1::2]  # every other element, starting on index 1 -> [2,4,6]
l[1::3]  # every 3rd element, starting on index 1 -> [2,5]

Even more slicing

l = [1,2,3,4,5,6]
l[::-1]    # all elements in reverse order -> [6,5,4,3,2,1]
l[::-2]    # every other element in reverse order -> [6,4,2]
l[4:0:-1]  # all elements from the 5th to the 2nd in reverse
           # -> [5,4,3,2]
l[-2:0:-1] # all elements from the 2nd last to the 2nd
           # in reverse -> [5,4,3,2]

Removing an element

The remove method deletes a given value from the list. If it exists more than once in the list, the first occurrence is removed.

temperatures.remove(37.0)  # removes the value

If the value does not exist in the list, an error is thrown.

Concatenate 2 lists

Adds to list l all elements from temperatures.

l.extend(temperatures)
l += temperatures

Counting

temperatures.count(36.8)  # how many times 36.8 occurs in the list
                          # -> 1

Check existence

if 36.8 in temperatures:
    print("Normal temperature reading found")

The in operator checks if the value on the left exists inside the data structure on the right.

Where is waldo?

pos = temperatures.index(36.8)
print(f"Found normal reading at index {pos}")

The .index method returns the position where the given value is in the list. If the value is not in the list, a ValueError is thrown (like .remove()).

Other methods

l.reverse()  # reverses the order of all values
l = l[::-1]  # same order as l.reverse()
l.sort()  # sorts all values
l.pop(0)  # removes and returns the element at index 0
l.pop()  # removes and returns the last element

l[::-1] gives the same order as l.reverse(), but not the same effect.1

Basic data structures: tuple

  • A tuple works exactly like a list, but doesn’t support assignment.
  • It is an imutable data structure.
  • We cannot change its value after creation.
t = (1,2,3,4)

t2 = 1,2,3,4

t[0]  # -> 1

t[0] = 7
    # TypeError: 'tuple' object does not support item assignment

Slicing (again…)

Slicing and concatenation work as in lists.

t = (1,2,3,4)
t[:-1] # -> (1,2,3)
t + (5,6) # _> (1,2,3,4,5,6)

tuple <-> list

We can easily convert between lists and tuples.

t = (1,2,3,4)
l = list(t)
t2 = tuple([5,6,7,8])

Mutable vs Immutable

Immutable objects (int, float, str, tuple) cannot be changed after creation.

Mutable objects (list, dict) can be changed in place.

The aliasing trap

Assigning a list to a new variable gives you a second name for the same object.

Lists are references, not copies. (This should be familiar to C programmers)

a = [1, 2, 3]
b = a           # b points to the same list as a
b.append(4)
print(a)        # -> [1, 2, 3, 4]  ← a was also changed!

To get an independent copy, use slicing or .copy():

b = a[:]        # or: b = a.copy()
b.append(4)
print(a)        # -> [1, 2, 3]  ← a is unchanged

+= vs +

a = [1, 2]
b = a
b += [3]        # in place: same list as a
print(a)        # -> [1, 2, 3]

a = [1, 2]
b = a
b = b + [3]     # new list, b now points to it
print(a)        # -> [1, 2]

Behind the scenes…

Check identity with id()

a = [3, 14, 15]
b = a
c = a.copy()
# Note id values change each time the objects are created
print(id(a))  # output: 140071445118656
print(id(b))  # output: 140071445118656
print(id(c))  # output: 140071444619968

Unpacking lists and tuples

t = (1,2,3)
a,b,c = t  # now a=1, b=2, c=3

l = [4,5,6]
a,b,c = l  # a=4, b=5, c=6

l=[1,2,3,4,5,6]
a,b,*nums = l # a=1, b=2, nums=[3,4,5,6]

Unit 5

Loops

Loops

  • loops: while, for
  • range, enumerate, zip

Loops

What are they?

The while loop

They execute a set of instructions while a given condition is true:

is_coffee_ready = False
while is_coffee_ready != True:
    print("refuse to work")
    is_coffee_ready = check_coffee_status()

What is being used for the condition must change, otherwise we might have an infinite loop.

Let’s sum a list of numbers:

l = [1,2,3,5,8,13]
total, i = 0, 0
while i < len(l):
    total += l[i]
    i += 1
print(total)

Actually…

Python has the sum function, no need to reinvent the wheel.

total = sum(l)

To stop a loop, we can use the break instruction:

l = [1,2,3,5,8,13]
total, i = 0, 0
while True:
    total += l[i]
    i += 1
    if i >= len(l):
        break
print(total)

We can skip the rest of the current iteration with the continue instruction:

l = [1, 2, 3, 4, 5, 6]
even = []
for n in l:
    if n % 2 != 0:
        continue   # skip odd numbers, go to next iteration
    even.append(n)
print(even)  # -> [2, 4, 6]

The for loop

We had to declare a variable just for increasing the position of the list element, but we also have the for loop which makes this kind of logic easier.

l = [1,2,3,5,8,13]
total = 0
for i in range(len(l)):
    total += l[i]
print(total)

Here the range function returns a list from 0 to the size of the list l: [0,1,2,3,4,5]

Actually, range() returns an iterable that goes from a number to another higher number.

# we can convert this iterable to a list easily
list(range(3))       # -> [0,1,2]
list(range(2,6))     # -> [2,3,4,5] 
list(range(0,10,2))  # -> [0,2,4,6,8] 
list(range(10,0,-2)) # -> [10,8,6,4,2] 

We can convert this iterable to a list, but we don’t have to do it in the context of the for loop.

  • What is an iterable?
  • What is an iteration?

Lists have another property: they’re also iterables! That means we don’t even need the range function in the last example.

l = [1,2,3,5,8,13]
total = 0
for num in l:
    total += num
print(total)

enumerate

What is an enumeration?

Let’s now say we are given a list of patient records and want to print the position of records that include “fever”.

records = [
    "P001: fever, cough, fatigue",
    "P002: high blood pressure, dizziness",
    "P003: fever, cough, shortness of breath",
    "P004: mild headache only"
]

One way to solve this would be to use the range() function.

for i in range(len(records)):
    if "fever" in records[i]:
        print(i)

But enumerate makes life a lot easier.

for i, record in enumerate(records):
    if "fever" in record:
        print(i)
  • enumerate receives a list and gives us a sequence of (index, value) tuples
  • First element of each tuple is the index of each value, e.g. 0, 1, 2, ...
  • Second element is the value on that position, i.e. records[0], records[1], records[2], ...

Actually, enumerate returns an iterable (like range), so we convert it to a list to see its contents.

l = [2,4,6,8]
list(enumerate(l)) # -> [(0,2), (1,4), (2,6), (3,8)]

It also can receive any iterable, not just a list.

enumerate can also start on any number we want.

l = [2,4,6,8]
list(enumerate(l))           # -> [(0,2), (1,4), (2,6), (3,8)]
list(enumerate(l, start=-1)) # -> [(-1,2), (0,4), (1,6), (2,8)]

zip

Let’s now say we have a list of patient IDs and ages and want to print them.

patient_ids = ["P001", "P002", "P003"]
ages = [45, 52, 38]

Using what we know, we can implement it like this.

for i in range(len(patient_ids)):
    print(f"Patient {patient_ids[i]} is {ages[i]} years old.")

But the zip function can simplify this.

for pid, age in zip(patient_ids, ages):
    print(f"Patient {pid} is {age} years old.")

zip receives 2 or more lists and creates a new list where each element is a tuple with the values of the original 2 lists.

patient_ids = ["P001", "P002", "P003"]
ages = [45, 52, 38]
print(list(zip(patient_ids, ages)))
# -> [('P001', 45), ('P002', 52), ('P003', 38)]
patient_ids = ["P001", "P002", "P003"]
ages = [45, 52, 38]
print(list(zip(patient_ids, ages)))
# -> [('P001', 45), ('P002', 52), ('P003', 38)]

zip, like enumerate, actually returns an iterable - that’s why we converted it to a list before printing.

zip can receive any iterable, not just lists.

zip can receive any number of iterables as input, e.g. zip(l1, l2, l3, ...)

When the size of the input iterables is different, zip will return an iterable with the size of the shorter input.

patient_ids = ["P001", "P002", "P003"]
numbers = [1,2,3,4,5,6]
print(list(zip(patient_ids, numbers)))
# -> [('P001', 1), ('P002', 2), ('P003', 3)]

Unit 6

Dictionaries and comprehensions

Dictionaries and comprehensions

  • dictionaries
  • list and dict comprehensions

Basic data structures: dict

  • A dictionary (dict) is like a key/value database.
  • We can store any value we want and connect it to a key, which we will use to retrieve it.

creating a dict

models = {}  # empty dict
models = dict() # empty dict
models = {"LogisticRegression": 0.87}
country_codes = {351: "Portugal"}

The types of both keys and values can be anything (that is hashable)

We can initialize a dict with many values.

models = {
    "LogisticRegression": 0.87,
    "RandomForest": 0.92,
    "SVM": 0.89
    }

We can initialize a dict from a list of 2 element tuples.

models_lst = [("LogisticRegression", 0.87), ("RandomForest", 0.92), ("SVM", 0.89)]
models = dict(models_lst)

Actually, we can initialize it from an iterable, so we could have done something like this.

model_names = ["LogisticRegression", "RandomForest", "SVM"]
accuracies = [0.87, 0.92, 0.89]
models = dict(zip(model_names, accuracies))

We can have nested dicts too.

models = {
    "LogisticRegression": {"accuracy": 0.87, "f1_score": 0.85},
    "RandomForest": {"accuracy": 0.92, "f1_score": 0.91},
    "SVM": {"accuracy": 0.89, "f1_score": 0.87},
    }

Getting a value

models["LogisticRegression"]  # -> 0.87
models.get("LogisticRegression")  # -> 0.87

models["XGBoost"]  # KeyError: 'XGBoost'
models.get("XGBoost")  # -> None
models.get("XGBoost", -1) # -> -1

Inserting a value

models["NaiveBayes"] = 0.81
models["KNN"] = 0.84

Removing a value

models.pop("NaiveBayes") # removes key "NaiveBayes" and returns its value
                         # -> 0.81
models.pop("XGBoost") # KeyError: 'XGBoost'
models.pop("XGBoost", -1) # tries to remove key "XGBoost" and
                          # returns -1 if it does not exist
del models["LogisticRegression"] # just deletes key "LogisticRegression"
del models["XGBoost"] # KeyError: 'XGBoost'

Useful methods

list(models) # returns list of keys
list(models.keys()) # same thing
list(models.values()) # list of dict's values
list(models.items()) # list of (key, value) tuples

dict and loops

Print the accuracy for each model:

for k in models: # by default, the for iterates over keys
    print(f"{k} achieved accuracy {models[k]}")
for k,v in models.items():
    print(f"{k} achieved accuracy {v}")
for i, (k,v) in enumerate(models.items()):
    print(f"model #{i}: {k} achieved accuracy {v}")

Comprehensions

list

Let’s say we would like to create a list with all even numbers from 0 to 99, that are not divisible by 4.

A possible implementation would be

l = []
for i in range(100):
    if i % 2 == 0 and i % 4 != 0:
        l.append(i)

List comprehensions are a way to concisely create lists.

l = [i for i in range(100) if i % 2 == 0 and i % 4 != 0]
l2 = [f"Patient {pid}: {age} years old" for pid,age in zip(patient_ids, ages)]

dict

Create a dictionary with the models that have accuracy above 90%.

top_models = {name: acc \
                for name, acc in models.items() \
                if acc > 0.90}
# \ allows breaking a long instruction into several lines

Unit 7

Functions

Functions

  • functions: definition, return values
  • variable scope
  • functions as first class citizens, lambda
  • default arguments, *args, **kwargs
  • type annotations

Functions

How to reuse the same logic for different things?

We can think of a function as a black box that receives inputs and gives outputs.

What functions have we seen before?

enumerate(), zip(), range(), int(), float(), str()

Function definition

def sum_2(a):
    b = a + 2
    return b
  • we define a function with the def keyword
  • followed by the name of the function sum_2 and ( )
  • inside the ( ) we write the names of the inputs (a.k.a. arguments) the function should receive.
  • the function exits when it reaches the end of the instructions or when the instruction return is used
def sum_2(a):
    b = a + 2
    return a, b

return can receive multiple values - which means the function will output multiple values

Remember tuples?

When we return multiple values like this a, b, we’re actually returning a tuple!

def return_ab():
    return 1,2
type(return_ab()) # -> tuple

Let’s use our function.

x = 3

def sum_2(a):
    b = a + 2
    return b

print(sum_2(x))  # -> 5

We call a function by writing its name, followed by ( ). Inside the ( ) we give the arguments (inputs).

We call sum_2 with input x. Inside sum_2, the argument a refers to the value of x.

Passing a list

Remember aliasing? The argument is another name for the same object.

def add_reading(readings):
    readings.append(37.5)

temperatures = [36.8, 37.2]
add_reading(temperatures)
print(temperatures)  # -> [36.8, 37.2, 37.5]  ← changed!

Numbers and strings are immutable, so a function cannot change them this way.

Scope of variables

We can have local and global variables

x = 3

def sum_2(a):
    b = a + 2
    print(x) # -> 3
    return b

print(sum_2(x))  # -> 5
print(x) # -> 3
print(a)  # -> NameError: name 'a' is not defined

a and b exist only inside function sum_2.

But x exists outside the function too.

x is unchanged by calling sum_2

  • a and b are said to be local variables, within the scope of the function
  • x is a global variable, because it exists outside the scope of any function
x = 3

def sum_2(a):
    b = a + 2
    x = 4
    print(x) # -> ?
    return b

print(sum_2(x))  # -> 5
print(x) # -> ?

Now what?

x = 3

def sum_2(a):
    b = a + 2
    x = 4
    print(x) # -> 4
    return b

print(sum_2(x))  # -> 5
print(x) # -> 3
  • Now we’re writing to x inside the function
  • But x=4 inside the function creates a new local variable x with the same name as the global variable x
  • every use of x inside the function expects a variable named x within the scope of that function

If we wanted to change (and not only read) the global variable, we need to declare it.

x = 3

def sum_2(a):
    global x
    b = a + 2
    x = 4
    print(x) # -> 4
    return b

print(sum_2(x))  # -> 5
print(x) # -> 4

All global variables are stored inside a dictionary that can be accessed by calling globals().

What would happen if we tried to change a global variable without declaring it?

x = 3

def sum_2(a):
    b = a + 2
    x += 2
    x = 4
    print(x)
    return b

print(sum_2(x))  # -> 5
      4     b = a + 2
----> 5     x += 2 # we try to change the value of the global variable x
      6     x = 4
      7     print(x)
      8     return b

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

Functions as first class citizens

In Python, functions are just like any other object that can be passed around.

In [65]: sum_2
Out[65]: <function __main__.sum_2(a)>

We can even attribute a function to a variable and then call the same function with the name of the new variable.

def sum_2(a):
    b = a + 2
    return b

f = sum_2
f(2) # -> 4

We may also pass a function as an argument to another function.

def apply_function(func, arg):
    y = func(arg)
    return y

apply_function(sum_2, 2) # -> 4

lambda

There is a shortcut for defining short and simple functions:

f = lambda a: a + 2
f(2) # -> 4

g = lambda a,b: a+b**2
g(2,3) # -> 11
  • We write lambda followed by the arguments we want to receive.
  • After the :, we write the expression that will be returned (no need to write return).

Why is this useful? Because we can pass custom operations to other functions and apply them elsewhere.

We can also import other functions and apply them in our codebase.

We’ll see 2 particularly useful examples of using functions in this way, map and filter, in the next session.

Default arguments

Arguments can have default values.

def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

greet("Ana")           # -> Hello, Ana!
greet("Ana", "Hi")     # -> Hi, Ana!

Arguments with defaults must come after those without.

*args — variable positional arguments

*args collects any number of positional arguments into a tuple.

def total(*args):
    return sum(args)

total(1, 2, 3)    # -> 6
total(10, 20)     # -> 30

**kwargs — variable keyword arguments

**kwargs collects any number of keyword arguments into a dict.

def describe(**kwargs):
    for k, v in kwargs.items():
        print(f"{k}: {v}")

describe(name="Ana", age=30)
# name: Ana
# age: 30

Combining *args and **kwargs

def log(message, *args, level="INFO", **kwargs):
    print(f"[{level}] {message}")
    if args:
        print("extra args:", args)
    if kwargs:
        print("extra info:", kwargs)

log("started", level="DEBUG", user="Ana")
# [DEBUG] started
# extra info: {'user': 'Ana'}

Type annotations

Python allows annotating the types of variables, arguments and return values.

def greet(name: str, greeting: str = "Hello") -> None:
    print(f"{greeting}, {name}!")

def total(*args: float) -> float:
    return sum(args)

Annotations are not enforced at runtime — Python does not check them.

Why use them anyway?

  • Better readability: the intent is clear
  • IDE auto-complete and inline warnings
  • Static analysis tools like mypy can catch type errors before running

Use them in function signatures when they make the code clearer.

Recap

Recap: Lists and tuples

  • lists, tuples
  • slicing
  • mutable vs immutable
  • unpacking

list

l = [1,2,3,4,5]  # creating a list
l[1] # getting an element ->2
l.append(6) # adding value 6 -> [1,2,3,4,5,6]
l.remove(1) # removing value 1 -> [2,3,4,5,6]
l.index(3)  # finding value 3 -> 1
l = l + [7,8] # appending lists -> [2,3,4,5,6,7,8]
l.extend([7,8]) # does the same thing
l.insert(0, 42) # add 42 at index 0 -> [42,2,3,4,5,6,7,8]
len(l)      # size of list 8
l[0] = 24 # change value at index 0 -> [24,2,3,4,5,6,7,8]
l[2:-2] # slice the list -> [3,4,5,6]
24 in l # -> True

tuple

t = (1,2,3,4)
t2 = 1,2,3,4
t[0]  # -> 1
t[0] = 7 # TypeError: 'tuple' object does not support item assignment
t[:-1] # -> (1,2,3)
t + (5,6) # _> (1,2,3,4,5,6)

unpacking

t = (1,2,3)
a,b,c = t  # now a=1, b=2, c=3

l = [4,5,6]
a,b,c = l  # a=4, b=5, c=6

l=[1,2,3,4,5,6]
a,b,*nums = l # a=1, b=2, nums=[3,4,5,6]

Recap: Loops

loops: while

l = [1,2,3,5,8,13]
total, i = 0, 0
while i < len(l):
    total += l[i]
    i += 1
print(total)

loops: for

l = [1,2,3,5,8,13]
total = 0
for num in l:
    total += num
print(total)

break and continue

# break: exit the loop early
for n in range(10):
    if n == 5:
        break
    print(n)  # prints 0 1 2 3 4

# continue: skip to the next iteration
for n in range(6):
    if n % 2 != 0:
        continue
    print(n)  # prints 0 2 4

range

l = list(range(10,15))
 [10,11,12,13,14]

enumerate

list(enumerate(l))
[(0, 10), (1, 11), (2, 12), (3, 13), (4, 14)]

zip

names = ["ten", "eleven", "twelve", "thirteen", "fourteen"]
l = [10, 11, 12, 13, 14]
list(zip(names, l))
[('ten', 10),
 ('eleven', 11),
 ('twelve', 12),
 ('thirteen', 13),
 ('fourteen', 14)]

enumerate in loops

for i, phrase in enumerate(phrases):
    if "Python" in phrase:
        print(i)

zip in loops

for name, age in zip(names, ages):
    print(f"The writer {name} died with {age} years.")

What is the pattern?

Combining with unpacking!

Recap: Dictionaries and comprehensions

list comprehensions

l = []
for i in range(100):
    if i % 2 == 0 and i % 4 != 0:
        l.append(i)

is equivalent to

l = [i for i in range(100) if i % 2 == 0 and i % 4 != 0]

dict

authors = {
    "Saramago": 87,
    "Pessoa": 47,
    "Camões": 56
    }

authors_lst = [("Saramago", 87), ("Pessoa", 47), ("Camões", 56)]
authors = dict(authors_lst)

authors = {
    "Saramago": {"died_at": 87, "first_name": "José"},
    "Pessoa": {"died_at": 47, "first_name": "Fernando"},
    "Camões": {"died_at": 56, "first_name": "Luís"},
    }

dict: get, set, remove

authors["Saramago"]  # -> 87
authors.get("Saramago")  # -> 87

authors["Vieira"] = 89
authors["Queirós"] = 54

authors.pop("Saramago") # removes key "Saramago" and returns its value
authors.pop("Asimov", -1) # returns -1 if key "Asimov" does not exist
del authors["Pessoa"] # just deletes key "Pessoa"

dict and loops

for k in authors:
    print(f"{k} died at age {authors[k]}")

for k,v in authors.items():
    print(f"{k} died at age {v}")

for i, (k,v) in enumerate(authors.items()):
    print(f"author #{i}: {k} died at age {v}")

dict comprehensions

authors_e = {name: age \
                for name, age in zip(names, ages) \
                if "e" in name}
# \ allows breaking a long instruction into several lines

Recap: Functions

functions

def sum_2(a):
    b = a + 2
    return b
    # return a, b
  • the function exits when it reaches the end of the instructions
  • or when the instruction return is used
  • functions can return multiple values

variable scope in functions

Local and global variables

x = 3

def sum_2(a):
    b = a + 2
    print(x) # -> 3, x is global variable
    return b

print(sum_2(x))  # -> 5
print(x) # -> 3
print(a)  # a is local variable of function 
          # -> NameError: name 'a' is not defined

first class functions

functions are also objects that can be given to variables and functions

def sum_2(a):
    b = a + 2
    return b

f = sum_2
f(2) # -> 4

lambda

f = lambda a: a + 2
f(2) # -> 4

g = lambda a,b: a+b**2
g(2,3) # -> 11

⚠️⚠️ keep your lambdas short, simple and clear ⚠️⚠️

default arguments, *args, **kwargs

def greet(name, greeting="Hello"):  # default argument
    print(f"{greeting}, {name}!")

def total(*args):           # any number of positional args -> tuple
    return sum(args)

def describe(**kwargs):     # any number of keyword args -> dict
    for k, v in kwargs.items():
        print(f"{k}: {v}")

type annotations

def greet(name: str, greeting: str = "Hello") -> None:
    print(f"{greeting}, {name}!")

def total(*args: float) -> float:
    return sum(args)
  • Not enforced at runtime
  • Improve readability, IDE support, and static analysis (mypy)
// multiplex-patch.js // Patches the reveal.js multiplex plugin to also broadcast: // - fragment navigation (fragmentshown / fragmenthidden) // - slide changes for headingless slides (re-emits slidechanged robustly) // // Only activates on the master (speaker view) — clients are unaffected. // Include in your Quarto frontmatter under revealjs.include-after-body // or revealjs.include-in-header (after the multiplex plugin loads). Reveal.on('ready', () => { const config = Reveal.getConfig().multiplex; // Only patch the master (speaker view has the secret) if (!config?.secret) return; const socket = io.connect(config.url); const broadcast = () => { socket.emit('multiplex-statechanged', { state: Reveal.getState(), // includes indexh, indexv, indexf, paused, overview secret: config.secret, socketId: config.id, }); }; Reveal.on('fragmentshown', broadcast); Reveal.on('fragmenthidden', broadcast); // Re-emit on slidechanged to catch headingless slides that the // stock plugin may miss due to index resolution timing Reveal.on('slidechanged', broadcast); });