Lists

It’s very rare we work with just a couple of variables at a time, more common is we work with a set of data. Consider the following case where we want to write a program to email 70 people.

email("samantha@fake.com")
email("bekky@fake.com")
email("greg@fake.com")
email("nina@fake.com")
email("whats_his_name@fake.com")
...

This is really just us doing something for every item in a list, the list being the list of people we want to email. We can skip the middle man and represent a list of data in python by using the square brackets

l = []       # l is a empty list
l = [1,2,3]  # l has 3 elements, the first element in 1, second is 2 third is 3

You can thus do

l = ["samantha@fake.com","bekky@fake.com",...]

and then to access lets say the first element we can do

l[0]    # get the first element (in computer science we count from 0)

Now this doesn’t help since our code is just

email(l[0])
email(l[1])
...

not much better, this is where we do looping

Looping

for user in l:
    print(user)

will print out every element in the list and thus every email, we can thus do

for user in l:
    email(user)

Even better is since all of our emails are under @fake.com we can shorten the list to

l = ["samantha","greg",...]

And then do

for user in l:
    email(user + "@fake.com")

There are other ways to loop, where a for loop will go through every item in a list we also have a while loop which does something over and over again as long as some conidition is true

while profits < 0:
    trade()

Now in either case sometimes you want to end early, you want to exit the loop or sometimes you just want to skip some item lets say if we want to email everyone in a list as long as they arn’t in a blacklist of enemies. This requires two things, one we need to check if the user is in a blacklist, and second we need to skip the user if they are

blacklist = ["greg"] # he called me ugly
for user in l:
    if user in blacklist:
        continue
    email(user)

The continue keyword stops the code where it is and jumps to the next item in the list
Now what if the moment you see greg you just stop, nobody is getting invited after you see greg, well here you can use break

blacklist = ["greg"] # he called me ugly
for user in l:
    if user in blacklist:
        break
    email(user)

That works but what happens if we have nested loops, lets say

for x in range(10):          # range(x) is a function that returns a list [0,1,2,3,4,...x-1]
    for y in range(10):
        print(x,y)
        break

Will break end the inner most loop or break both loops?
It’ll break just the innermost loop so be careful.

Also note that lists can grow and shrink as needed, you can do l.append(x) to put a element on teh end of a list and l.pop() to pop a element off the front for example

List Comprehension

Now the whole idea of “do X for every element in Y only if Z” is super common

As such you can do this in one line within python

l = [X for X in Y if Z]

is how you can create a list with all the elements in Y which meet some criteria Z. It basically helps you filter a list down.

friends = ["greg","simon","samantha"]
blacklist = ["greg"]
invited = [friend for friend in friends if friend not in blacklist]

And you can even apply extra transformations and ignore the filtering aspect

l = [1,2,3,4]
doubled = [2*x for x in l]
print(doubled) # prints out [1,4,6,8]

Type

A list is just another type, and some things can be converted into lists as a result, for example

s = "hello"
list(s) # this becomes ['h','e','l','l','o'] a character list.

An interesting feature of python is the ability to actually multiple strings. Python will do the work of taking a string and repeating it n number of times for you

>>> "A"*10
"AAAAAAAAAA"
>>> "na"*8 + " batman"
"nananananananana batman"

Multi-Dimentional lists

You can put anything into a list, a number a string or in fact another list.

If you wanted a 5 by 5 table it would look something like this

table = [
    [1,2,3,4,5],
    [6,7,8,9,10],
    [11,12,13,14,15],
    [16,17,18,19,20],
    [21,22,23,24,25]
]

now l[1] would return [6,7,8,9,10] and thus l[1][0] would be 6, row 1 col 0.

Reference

Now the interesting thing is that lists behave a bit differently the normal variables.

l = []
add_to_l(l):
    l.append(1)
print(l)

with a normal variable the value gets copied before the function starts so all the changes are disgarded, but the way lists work is that you pass a reference to the list around so any changes you make on the list anywhere are global unless you explicitly copy the list.

What do you think this will print

l = []
l2 = l
l.append(1)
print(l)
print(l2)

What about this

l = []
l2 = l.copy()
l.append(1)
print(l)
print(l2)

Tuples And sets

Another way to represent a collection of data is to use a use a set which ar exactly like mathematical sets in that they can not have duplicates or any order

s = set()
s.add(1)
s.add(1)
len(s) == 1 # true

These are nice cause you can do unions, intersections etc. on them, check out the docs for a full list

s = s1 | s2 # union
s = s1 & s2 # intersection

There are also tuples, these are the same as lists but once created are locked

l = [1,2] # list
t = (1,2) # tuple
l.append(1) # all good
t.append(1) # Error

Sorting

It’s really common you want to sort some list by value and python makes that easy

l = [1,6,2,3,8,1]
l = sorted(l)
print(l)          # [1,1,2,3,6,8]

But sometimes you want to sort a list of a more complex object, lets say you have a list of tuples and you want to sort by the second field in each tuple.
You can do this by specifing a function you want to be run on each element of a list to extract out the field to be sorted on, basically python will take your function f and do f(e) for each item in the list before sorting it

def extract_second(e):
    return e[1]
l = [(1,10),(2,100),(3,0)] # we want [(3,0),(1,10),(2,100)]
l = sorted(l, key=extract_second)

Now this is a very simple 1 line function so we can use a lambda

l = [(1,10),(2,100),(3,0)] # we want [(3,0),(1,10),(2,100)]
l = sorted(l, key=lambda x: x[1])

Splicing

this is a way you can extract a set of values from a list

>>> l = [1,2,3,4,5,6,7,8,9]
>>> l[0:9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[0:8]
[1, 2, 3, 4, 5, 6, 7, 8]
>>> l[1:8]
[2, 3, 4, 5, 6, 7, 8]
>>> l[1:2]
[2]
>>> l[1:5]
[2, 3, 4, 5]
>>> l[1:]
[2, 3, 4, 5, 6, 7, 8, 9]
>>> l[:9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[:]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[:3]
[1, 2, 3]

You can also extract every nth value by specifying a third number in your splice

>>> l = [1,2,3,4,5,6,7,8,9]
>>> l[0:9:2]
[1, 3, 5, 7, 9]
>>> l[0:9:3]
[1, 4, 7]
>>> l[0:9:3]
[1, 4, 7]
>>> l[0:9:9]
[1]

Dictionaries

These are SUPER useful data structures in python, they let us map one thing to another. Consider this

d = {}          # make a empty dictionary
d["zain"] = 2   # the string "zain" maps to the number 2

The way these work in the backend make them not only very nice to use but also super quick. Whenever you want to have a way to convert one thing into another quickily use a dict first. It also really helps for counting things, consider this example

d = {}
d["apple"] = 0
d["pear"] = 0
for x in ["apples","pear","pear","pear","apple"]:
    d[x]+=1
print(d)

We’ll print out the dictionary and it’ll say d["apple"]: 2, helping us count the number of each item in a list.

Do note of course these are case sensitive “Apple” != “apple”

you can have really anything map to anything else, the thing you put in the brackets is called the key and the thing it maps to is called a value

# you can also declare some values into a dict at time of creation
d = {
    "hello": 1     # the key "hello" maps to the number 1,
    1: "what"      # the number 1 maps to the string "what"
}

note that the syntax is the same but dicts are NOT lists

l = []
d = {}
l[0] = 1 # 0th element is 1
d[0] = 1 # the number 0 maps to 1

You may think why use a dict if you want to map a index to something. why not just use a list?

What do you think will happen here

l = []
l[100] = 1
print(l)
d = {}
d[100] = 1
print(d)

The line l[100] = 1 will fail, the list doesn’t have 100 elements, you’d have to make a list with 100 cells and set the 100th one to 1, that’s wasteful if you only want to store the value for 100.

Ok so why not use a dict everywhere? Dicts are bad for iterations, it’s annoying to append increasing values to them and if you want to iterate over all the values you have to do

for value in d.values():
print(value)

and until recently your output would be jumbled, i.e in no paritcular order (a recent update actually made it that dicts always return values in the order they were inserted but that’s somewhat cutting edge)

In addition lists are better then dicts when it comes to reading and writings sets of data.

Lastly if you try and get a value with a key that the dict doesn’t know about you get a key error but you can check for a key very easily

d = {"zain": 1}
if "zain" in d:
    print(d["zain"])

Even easier you can use get to get a value and if it doesn’t exist return a default instead of crashing

print(d.get("zain",None)) # prints value of zain if it exists otherwise print None

Dict Comprehension

You can also form dictionaries quickily by doing

d = { i: i*2 for i in range(3)}
print(d)
{
    0: 0,
    1: 1,
    2: 4
}

Same as list Comprehension but with curly braces.