A little more about slicing:
Recall the list slicing notation, which lets us get a sublist of a list.
xs = [1,2,3,4,5,6,7]
print(xs[2])
print(xs[2:4]) # from 2 to 4
print(xs[:2]) # up to 2
print(xs[2:]) # starting from 2
print(xs[:]) # everything
# you can also put an increment amount
xs = list(range(20))
print(xs[0:10:3])
print(xs[1:15:2])
An important thing about it is that the slicing notation actually makes a new list.
# compare this:
xs=[1,2,3,4]
ys = xs
ys[0] = 0
xs
# to this:
xs=[1,2,3,4]
ys = xs[0:4] # could also have written ys=xs[:]
ys[0] = 0
xs
changing ys didn't change xs, which means that it's a new list in a separate memory location
Like, you know, tuples.
t = (2,3)
t[0]
t[1]
t = (2,3,4)
print(list(range(10)))
print(tuple(range(10)))
So tuples are just like lists. What's the difference? Here it is:
t = (2,3,4)
t[0] = 999
Once a tuple is made, it can never be changed. So tuples are immutable. That's why lists and tuples are different things.
Cool use of tuples: making functions return multiple values at the same time.
def division_alg(a,b):
return (a//b, a%b)
(q, r) = division_alg(11,3)
print(q,r)
You can omit the parantheses around for a more slick look:
def division_alg(a,b):
return a//b, a%b
q, r = division_alg(11,3)
print(q,r)
list(zip([1,2,3,4], ["a","s","d","f"]))
Many times it is very natural for a function to call itself. For example, we all know: $$n! = n.(n-1)!$$
def f(n):
if n <= 1:
return 1
return n*(f(n-1))
What happens when I call f(5)
? It looks at the definition of f
and realizes it needs to evaluate 5*f(4)
, so it looks at the definition of f
and realizes it needs to evaluate 4*f(5)
,...
and then eventually it comes to f(1)
. It does not call itself anymore beucase it returns 1
. Then it goes back up and finishes all the evaluations it was doing.
# let's print the first 20 values:
for i in range(20):
print(f(i), end=" ")
What would happen if we didn't have the if statement?
def factorial(n):
return n*(factorial(n-1))
If would never stop calling itself again and again and it would basically be like having an infinite loop.
Let's do another example:
# nth fibonacci number:
def fibonacci(n):
if n <= 2:
return 1
return fibonacci(n-1) + fibonacci(n-2)
for i in range(1,21):
print(fibonacci(i), end=" ")
This is actually very very inefficient. Can you say why? Make a diagram of which fibonacci(m)
's are called by which ones, you will see that each fibonacci is called multiple times. It will take $\operatorname{O}(2^n)$ function calls to compute fibonacci(n)
.
We want to write a function that returns the reversed version of a list. (In HW3, we had done in-place reversing using a for loop, now we just want to return the reversed list)
The idea is this:
reversed list = (last element of list) + (reverse of the rest of the list)
def reverso(xs):
if len(xs) == 1:
return xs
return [xs[-1]] + reverso(xs[:-1])
Alternative:
def reverso(xs):
if xs == []:
return xs
return [xs[-1]] + reverso(xs[:-1])
Note that this is not an efficient way of doing it.
xs = list(range(100))
print(reverso(xs))
The idea of using recursion is not to use for loops.