In this class you are expected to learn:
Sometimes you want to make a big library of functions. And you'd like to access some of those functions from another program that you're writing.
If you put your functions in a file called myfuncs.py
(using your favourite plain text editor), you can import them into another program like this:
>>> from myfuncs import *
The * here means everything
You could also use:
>>> import myfuncs
But, this adds a namespace. To access a function called dostuff
in the file myfunc.py
after this style of import, you'd have to type:
>>> myfuncs.dostuff(...)
There is a huge amount of libraries creted by the community around Python. Go and check PyPI (Python Package Index), where all the libraries live.
In [5]:
from mylib import maximum, minimum
Activity
Write your own library called `mylib` with two functions: `maximum`, that takes two values as arguments and returns the greatest from both; and `minimum`, that returns the least. And now place the file `mylib.py` in the same [path](http://cli.learncodethehardway.org/book/) where you are running IPython. If everything is OK, the next cell should work.
In [6]:
from mylib import maximum, minimum
print(minimum(maximum(4, 9), maximum(13, 47)))
Python has a variety of built-in functions including max()
, min()
, sum()
, pow()
, abs()
, sqrt()
that do exactly what you expect them to do.
Comments are very important, so use them in your programs.
They are used to tell you what something does in your own language, and they also are used to disable parts of your program if you need to remove them temporarily. Here's how you use comments in Python:
# A comment, this is so you can read your program later.
# Anything after the # is ignored by python.
print "I could have code like this." # and the comment after is ignored
# You can also use a comment to "disable" or comment out a piece of code:
# print "This won't run."
print "This will run."
When working on bigger projects, and for your own general sanity, you want to add comments to your functions explaining what it does, what parameters it needs, and what it returns. For documenting functions we use the triple quote, """
, to enclose the content of the comment, which allows us to write multi-line comments.
Also, for paremeters, we follow the same rule:
def celsius_to_fahrenheit(degrees):
"""
Compute the value in Fahrenheit of the
given Celsius degrees.
:param degres: float or integer with the Celsius degrees
:return: float expressin Fahrenheit degrees
"""
return 9.0 / 5.0 * degrees + 32
You may think that documenting code is kinda lame, but it gives you future-proof code against our weak memories. And as a nice side-effect, you can generate beautiful HTML or PDF documentation just by processing your source code, in the same way that IPython will include your docstrings (short for document strings) in its interface. There are plenty of tools to document your code; the official in the Python community is Sphinx, though.
In [20]:
def celsius_to_fahrenheit(degrees):
"""
Compute the value in Fahrenheit of the
given Celsius degrees.
:param degrees: float or integer with the Celsius degrees
:return: float expressing Fahrenheit degrees
"""
return 9.0 / 5.0 * degrees + 32
In [21]:
celsius_to_fahrenheit(25) # Put the cursor between the parenthesis and press the Tab key
Out[21]:
In order to ensure that a function is defined before its first use, you have to know the order in which statements are executed, which is called the flow of execution. The Python interpreter executes one statement at a time.
To make sense of programs, we need to know which instruction gets executed when.
In a program, the statements get executed in the order in which they appear in the program, top to bottom of the file. Later, we’ll learn how to jump around.
On the other hand, IPython allows you to create cells anywhere in the notebook, but internally, they are all executed one after the other, as well as inside the cells.
But what happens when a function gets called? Let's trace through this program:
In [2]:
from IPython.display import IFrame
IFrame("http://pythontutor.com/iframe-embed.html#code=x+%3D+20%0Ay+%3D+10%0A%0Adef+half(a)%3A%0A++++return+a+/+2%0A%0Adef+divide_half(a,+b)%3A%0A++++half_a+%3D+half(a)%0A++++return+half_a+/+b%0A%0Adivide_half(x,+y)&cumulative=false&heapPrimitives=false&drawParentPointers=false&textReferences=false&showOnlyOutputs=false&py=3&curInstr=0&codeDivWidth=350&codeDivHeight=400", width=800, height=500)
Out[2]:
Try now executing the code in the cell
In [ ]:
x = 20
y = 10
def half(a):
return a / 2
def divide_half(a, b):
half_a = half(a)
return half_a / b
divide_half(x, y)
Activity
Write a function, `distance`, that returns the distance between two points, $P_1$ and $P_2$. The coordinates of the points are given by its components: $P_1 = (x1, y1)$, and $P_1 = (x2, y2)$; `distance` receives *4* parameters.
Remember that the formula to calcualte the distance is $d = \sqrt{(x2 - x1)^2 + (y2 - y1)^2}$.
*Hint*: $\sqrt{a}$ is the same that $a^{\frac{1}{2}}$. How do we do exponentiation in Python? And square roots?
In [29]:
def distance(x1, y1, x2, y2):
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** (0.5)
distance(1, 3, 8, 9)
Out[29]:
So far, the only thing we can do is to write statements one after the other. Conditional statements give us this ability to check conditions and change the behavior of the program accordingly.
So, if
some expression is True
, then do something; otherwise do something else
.
The syntax in Python is as follows:
if expression:
# Code in this block is evaluated only if expression is true
print("Expression is true!")
else:
# This statements however, are evaluated only if expression is false
print("Expression is false!")
The second part, the else
, is optional. And it can be extended to support more else
-cases.
In [ ]:
number = int(input("Choose a number: "))
if number < 5:
print("It's lower than 5")
elif number < 8:
print("It's lower than 8")
else: # Greater than 5 and greater than or equal to 8
print("It's at least 8 or greater")
Activity
Write a function, `print_parity`, that prints `"x is even"` when its only parameter, `x` is even, and `"x is odd"` when `x` is odd
In [4]:
def print_parity(x):
# Writes your code here
print_parity(9)
print_parity(20)
And of course, if
statements can be nested as deep as you need.
In [6]:
x = 6
if 0 < x:
if x < 10:
print("x is a positive single digit.")
Although we can re-write the same code using only one statement.
In [7]:
if 0 < x and x < 10:
print("x is a positive single digit.")
And now even more complicated, without using and
.
In [8]:
if 0 < x < 10:
print("x is a positive single digit.")
Readability counts since programs are read and modified far more often than they are written.
There is a style guide developed by the Python community known as PEP8, that every Python programmer should use. Besides using a docstring for your functions, we will consider the following:
If the only way we had to repeat tasks were writing the same statement over and over, programming wouldn't be the such awesome and fun thing that indeed is. To repeat blocks of code we use loops, and each repetition is called an iteration.
The first kind of loop, the most basic one, is the while
loop, in which all the code is executed while a certain logical condition is met.
Let's see an example.
In [37]:
x = 1
while x < 0:
print(2*x)
x += 1
In the last example, the condition is the logical expression x < 10
, so x
is the variable that need to increment in order to get out of the loop. If we remove it, we will end up with an infinite loop.
In while
loops is super important to worry about how to stop them, and we do this by modifying the variables involved in the condition.
In [43]:
def num_digits(n):
count = 0
print("count", count)
print("condition", n)
while n:
count = count + 1
print("count", count)
n = n // 10 # Integer division
print("condition", n)
return count
num_digits(122345678767)
Out[43]:
In the function num_digits()
, we see that as a condition we just have the variable n
. That's possible because, as in if
statements, those expressions are actually evaluated as boolean by using internally the bool()
casting. So you can also write the function as follows.
In [11]:
def num_digits(n):
count = 0
while bool(n):
count = count + 1
n = n // 10
return count
num_digits(123456789)
Out[11]:
Well, that's ugly and unnecessary. But what really improves the readibility of your code is to write whole expressions when possible. This is specially important when dealing with None
values.
In [44]:
def num_digits(n):
count = 0
while n != 0:
count = count + 1
n = n // 10
return count
num_digits(123456789)
Out[44]:
Activity
The [Collatz Conjecture](https://en.wikipedia.org/wiki/Collatz_Conjecture) (also known as the $3n+1$ conjecture) is the conjecture that the following process is finite for every natural number: if the number $n$ is even divide it by two, $n/2$, if it is odd multiply it by 3 and add 1, $3n + 1$. Repeat this process untill you get the number 1.
We want a function, `collatz`, that given an arbitrary number, `n`, as the argument, prints all the intermediate values until reach `1`. For example, if the starting value (the argument passed to sequence) is 3, the resulting sequence is 3, 10, 5, 16, 8, 4, 2, 1.
In [51]:
def collatz(num):
n = num
while n != 1:
print(n, end=", ") # what does that 'end' argument?
if n % 2 == 0: # n is even
n = n // 2
else: # n is odd
n = (3 * n) + 1
print(n)
collatz(30)
Updating the value of a variable is so so common that Python provides several shorcuts.
In [13]:
count = 0
count += 1
count
Out[13]:
In [14]:
count += 1
count
Out[14]:
In [15]:
n = 2
n += 5
n
Out[15]:
In [16]:
n = 2
n *= 5
n
Out[16]:
In [17]:
n -= 4
n
Out[17]:
In [18]:
n //= 2 # or n /= 2 for float division
n
Out[18]:
In [19]:
n %= 2
n
Out[19]:
A string is, in reality, a second class citizen in the world of Python types. It's what we call a compound type, because is built upon the basic types we've have seen.
But strings are fun. For example, we can access elements individually by using braces and the index or position in the string. And count the length of a string by using the built-in len()
. If we put all together we can actually print all the individual characters that form a string.
In [8]:
doge = "many smart"
length = len(doge)
index = 0 # First element is always at position 0!
while index < length:
character = doge[index]
print(index, character)
index += 1
In [21]:
doge[2:-2]
Out[21]:
Activity
Experiment with different indices in a string. What happens when we do `doge[-1]`? What if we use a slice like in `doge[2:4]`? What if `doge[:2]`?
BTW, iterables usually support the in
operator to check if an element is contained or not.
In [23]:
"clever" in doge
Out[23]:
Other times, strings can be used as templates in which to put other values. That's called formating.
In [25]:
"Hi, my name is {name}".format(name="Slim Shady")
Out[25]:
There is a while
loop pattern so common, that Python (as usual), already includes it as a built-in feature. In the case of compund objects that you can iterate over, the pattern is called traversal and is abstracted in the form of a for
loop.
Strings happend to be iterable, what means that you can iterate through them. So let's see how the above example works with a for
loop.
In [26]:
doge = "many smart"
for character in doge:
print(character)
The savings in code, as well as the awesomeness of the for
loops, are pretty obvious. And if you just don't get it, don't worry, we will see a lot more for
loops.
Activity
Write a function, `mult_tables(n)`, that prints multiplication tables for all numbers up to `n`, which is given as the argument
In [15]:
numInput = int(input("Enter a number: "))
rows = 0
counter = 0
columns = numInput
while (rows < 10): #while the number of rows is less than the number entered, first row is row 0
rows = rows + 1 #determines the starting number of every row - first row starts with 1
x = rows
columns = numInput #resets columns to make sure every row has the same amount
while (columns > 0): #while the number of columns is less than the number entered
print(rows, "x", (numInput - columns + 1), "=", x, "\t\t", end="")
x = x + rows #increments in a multiplication table row are by the first number of the row
columns = columns-1
print("\n\n") #new line
In [4]:
def mult_table(n):
count = 1
while count <= 10:
multiplication = count * n
print(count, "x", n, "=", multiplication)
count += 1
def mult_tables(n):
count = 1
while count <= n:
mult_table(count)
count += 1
mult_tables(3)
Lists are the the first real data structure. They are intended to store, in a sorted way, any number of any kind of elements.
In [27]:
l = [1, 2, 3]
l
Out[27]:
In [28]:
m = [1, "WAT!", None]
m
Out[28]:
And the same slicing and length rules apply to list in order to access their elements.
In [29]:
m[1]
Out[29]:
In [28]:
len(l) + len(m)
Out[28]:
In [30]:
l[8] # Well, this list is not that long
One difference here, unlike strings, lists are mutable, that means that once defined, we can change its values. Let's see how to delete eleents using del
.
In [51]:
l = [["Javi", "Spain"], ["Natalia", "Canada"], ["Adriana", "Spain"], ["Nandita", "India"]]
for name, country in l:
message = "My name is {nom}, and I am from {country}".format(nom=name, country=country)
print(message)
In [63]:
l = [["Javi", "Spain"], ["Natalia", "Canada"], ["Adriana", "Spain"], ["Nandita", "India"]]
["Natalia", "Canada"] in l
Out[63]:
In [53]:
l = [1, 2, 3, 4, 5, 6, 7, 8]
del l[3:6]
l
Out[53]:
In [54]:
del l[0]
l
Out[54]:
In [56]:
string = "hey bro! no worries"
string[5] = "5"
Lists, like almost everything in Python, are objects, and objects have methods. Methods are functions that only operate over the same object that are invoked from. Better with an example.
The list method append()
adds an element to the end of the list.
In [58]:
l.append("I see it now!")
l
Out[58]:
The method pop()
removes and returns the last item in the list. If an index is specified, it removes the item at the given position in the list, and return it.
In [32]:
l.pop()
Out[32]:
And index()
gives you the position of an element, if the element is member of the list. And remember, the first position is 0, not 1. In Python we start counting at zero, so the first element is the one that is in the position 0, the second in the position 1, and so no.
In [33]:
l.index(8)
Out[33]:
Checking for membership is as expected.
In [34]:
1 in m
Out[34]:
And remove()
removes the element from the list, again, if it's already a member.
In [35]:
l.remove(2)
l
Out[35]:
In [36]:
l.remove("not in the list")
The +
operator concatenates lists.
In [37]:
a = [1, 2, 3]
b = [4, 5, 6]
c = a + b
c
Out[37]:
Similarly, the *
operator repeats a list a given number of times.
In [38]:
[0] * 4
Out[38]:
In [39]:
[1, 2, 3] * 3
Out[39]:
And as I promised, more for
loops! Analyze carefully the output of the next cell.
In [40]:
lst = [1, 1.6, False, None, "sup, yo!", ["WAT!", 4]]
for item in lst:
print(item)
And one last important thing. When you assign lists, the content is not copied. Actually, what you are doing is creating another name for the same value.
In [64]:
l1 = [1, 2, 3]
l2 = l1
l2[1] = "OMG!" # This is how you can edit an element, remember
# We print l1 and... OMG!
l1
Out[64]:
But Python provides you with a way to copy the whole thing, a method for lists called, suprise surprise, copy()
In [42]:
l1 = [1, 2, 3]
l2 = l1.copy() # An equivalent way would be to use l1[:]
l2[1] = "OMG!"
# Print now l1 and... voilá!
l1
Out[42]:
Activity
Python `list`s have a method called `join()` that does exactly the opposite that `str`'s method `split()`. Play around to discover what they do.
In [70]:
"This is sentence one. This is sentence two from NYT.".split(".")
Out[70]:
In [75]:
s = ' This is sentence two from NYT '
s = s.strip(" ")
s
Out[75]:
In programming, there is the need for simple integer lists so that we can count things. In Python this is made easy through the range()
built-in function.
In [85]:
[i for i in range(0, 11, 2)]
Out[85]:
In [43]:
for i in range(10):
print(i)
Depending on Python version, may or may not return a list. But anyhow, it always returns something that you can iterate over. It also supports parameters, so you can count up or down, in steps, and starting and ending in specific values. For more, read the docs on range()
.
In [44]:
for i in range(10, -10, -2):
print(i)
In [45]:
for i in range(-10, 10, 2):
print(i)
Notice that while the first argument in taken into account, the second is not, so the resulting list/iterable won't included it.
We have seen that lists can be modified at any time. But what if you want your list to be the same all the time? That means that you want an immutable list, which is called a tuple()
.
In [46]:
t = (1, 2, "cats")
In [47]:
t[0]
Out[47]:
In [48]:
t[0] = 56 # Nah, nah, nah
So, besides the operations to modify lists, the rest of them are also applicable to tuples. And as a side-effect, tuples are way more efficient in memory than lists.
You can even convert tuples into list and back
In [49]:
tuple([1, 2, 3])
Out[49]:
In [50]:
list((1, 2, 3))
Out[50]:
In [51]:
print(type(tuple()), type(list()))
Activity
Write a function, `count_char(string, char)`, that counts the number of aparitions of the character `char` in the string `string`, and returns it.
For example, if we call `count_char("Mississippi", "s")`, the result must be `4`.
In [5]:
def count_char(string, char):
result = 0
for character in string:
if character.upper() == char.upper():
result += 1
return result
count_char("Mississippi", "m")
Out[5]:
All of this takes us to last built-in data type in Python: the dictionary. It's the most complex data structure we've seen so far, yet very powerful and useful.
You can think of a dictionary as a list in which the indices can be any (immutable) value, not just integer numbers, and map other values.
One way to create a dictionary is to start with the empty dictionary and add key-value pairs. The empty dictionary is denoted {}
, or we can also use the type dict()
. To add new key-value pairs, we use the []
notation, as in lists.
In [6]:
d = {}
d[0] = 'a'
d[1] = 'b'
d
Out[6]:
Or we can build the dictionary at once, as we do with lists or tuples, by separating each key from the value with a colon, :
, and then the pairs with commas, ,
.
In [7]:
d = {0: 'a', 1: 'b'}
d
Out[7]:
Let's create an example dictionary that we can use to translate numbers from English to Spanish.
In [9]:
trans = {
"one": "uno",
"two": "dos",
"three": "tres",
"four": "cuatro",
"five": "cinco",
}
In [16]:
trans
Out[16]:
In [10]:
trans["one"]
Out[10]:
In [11]:
"Yo quiero {number} hamburguesas".format(number=trans["three"])
Out[11]:
Dictionaries have some useful methods.
In [15]:
for key in d.keys():
print(key)
In [12]:
for key in d.keys():
print(key)
In [17]:
for value in d.values():
print(value)
In [18]:
for pair in d.items():
print(pair)
In [19]:
# Althougg is usually more useful if we split the key and the value
for key, value in d.items():
print(key, value)
Usually, if you try to retrieve the value associted to a key that doesn't exist in the dictionary, you'll see an error.
In [20]:
d["key"]
To avoid this, you could firts ask if the key already exists in the dictionary, and in that key, just return the value associated with.
In [21]:
"key" in d
Out[21]:
In [63]:
key = 0
if key in d:
print(d[key])
else:
print("Not here!")
The last pattern is so common that Python includes a shorthand for that.
In [22]:
d
Out[22]:
In [26]:
d.get(5, "not there")
Out[26]:
In [64]:
key = 1
d.get(key, "value if d doesn't contain the key")
Out[64]:
In [65]:
key = "nope"
d.get(key, "value if d doesn't contain the key")
Out[65]:
Activity
Write a function, `freq_dict(string)`, that counts the number of aparitions of each letter in the string `string`, and returns a dictionary with letters as keys and their frequencies as values.
For example, if we call `freq_dict("Mississippi")`, the result must be `{'M': 1, 's': 4, 'p': 2, 'i': 4}`
In [27]:
def freq_dict(string):
result = {}
for character in string:
if character in result:
result[character] += 1
else:
result[character] = 1
return result
freq_dict("Mississippi")
Out[27]: