Lecture Notes 14: Math module and Random,
plus Odds and Ends
Modules
As we've seen, we can make functions that can be used again and again.
This lets us write more efficient code!
Another approach is to use modules.
Modules are python files that contain functions that we can import and use in our programs.
There are two important steps in using modules:
- importing the module (from a known location)
- calling the correct function from the correct module
namespaces
A namespace is like a list of known objects.
A module is simply a namespace that contains definitions from the module (like functions defined in it).
Definitions in the module's code, e.g., variable assignments and function definitions, are placed in the module's namespace. The module is then added to the importing script or module's namespace, so that the importer can access the definitions.
Importing
If a module is placed in a location known to python (like the set of python modules or the
current working directory), we can simply import it in our code.
Step 1
For us, step 1 is to make sure we're either:
- using a common python module (like math or random (which are located in a known directory)
or
- copying the desired module into our current working directory
Step 2
You can import a module by using the
import reserved word and the name of the module (usually at the top of the file).
The syntax is:
import < module name >
Step 3
Finally, you can use a module function by using the name of the module and the
dot operator.
The syntax is:
< module name > . < function name and arguments >
In the following wections we'll see the
math and
random modules and how to use them.
The Math Module
See the functions in the math module
You can also read about it in https://realpython.com/python-math-module/
In the following program, we use three math functions:
import math
def main():
'''Using math-module functions'''
num1 = 24
n_sq1 = math.sqrt(num1)
print(f"the Square root of {num1} is {n_sq1:.2f}")
print()
num2 = 54
gcd = math.gcd(num1, num2)
print(f"the Greatest Common Divisor of {num1} and {num2} is {gcd:.2f}")
print()
num3 = 2
n_pow1 = math.pow(n_sq1,2)
print(f"the Square of {n_sq1} is {n_pow1:.2f}")
print(f"the Square of {n_sq1} is {n_pow1}")
print()
print("Some important 'constants' are: ")
print(" PI: ", math.pi)
print(" e: ", math.e)
if __name__ == "__main__":
main()
You can see the example
here
The from syntax
Another way to do it is by using the
from syntax:
using
from adds only the specified names to the global namespace.
Modifying the previous example
from math import *
def main():
'''Using math-module functions'''
num1 = 24
n_sq1 = sqrt(num1)
print(f"the Square root of {num1} is {n_sq1:.2f}")
print()
num2 = 54
n_gcd = gcd(num1, num2)
print(f"the Greatest Common Divisor of {num1} and {num2} is {n_gcd:.2f}")
print()
num3 = 2
n_pow1 = pow(n_sq1,2)
print(f"the Square of {n_sq1} is {n_pow1:.2f}")
print(f"the Square of {n_sq1} is {n_pow1}")
print()
print("Some important 'constants' are: ")
print(" PI: ", pi)
print(" e: ", e)
if __name__ == "__main__":
main()
you can play with this example
here
Note that in main, we're using functions defined in the math module but we don't need to use the math. notation
Also, in the python tutor example, you can see that all function definitions were loaded directly into the importing script's namespace
Now try importing ONLY the sqrt function:
from math import sqrt
In this class, for now, we'll use the notation: import math
and not
from math import *
This is because if we import multiple modules, we might have name clashes!
Brief intro to Random variables
A random variable is one whose outcome (the result of sampling the random variable) is unpredictable but whose outcome might follow some known distribution.
Example distributions:
- Uniform distribution: all possible outcomes are equally likely
Examples: fair coin tosses, fair dice rolls.
See an example here
- Normal distribution: outcome likelyhoods follow a Bell curve
Examples: random sampling of human heights , the summ of many fair dice rolls.
See an example here
The Random Module
You can import the random module by using:
And now an example:
import random
def main():
'''Using random-module functions'''
for i in range(10):
num = random.random()
print(f"random number {i:2d} : {num:.3f} ")
if __name__ == "__main__":
main()
You can run it
here
the function
random.random() generates a random number (float) in the range
[0, 1)
Activity 2 [2 minutes]:
Try the code in python tutor and write the code and run it in Replit.
Run it a few times? ... What do you notice?
Activity 3 [2 minutes]:
Write a program that simulates a coin toss...
it should allow you to run it and:
- half the time it should print the word "Heads"
- half the time it should print the word "Tails"
- the outcome should be random!!
Now, modify your code to do the following:
Activity 4 [2 minutes]:
Write a program that simulates a coin toss and:
it should:
- run 10 times
- it adds up the number of "Heads"
- it adds up the number of "Tails"
- prints the count of heads as in Heads : 4
- prints the count of tails as in Tails : 6
Now modify it to run
1,000,000 times!
Using function randint
You can also indicate a specific range of integer values as a range for your output:
import random
def main():
'''Using random-module functions'''
for i in range(30):
num = random.randint(4,13)
print(f"random number {i:2d} : {num:2d} ")
if __name__ == "__main__":
main()
You can run it
here
Activity 5 [2 minutes]:
Do you notice something strange?
(Wait; then Click)
the top number is INCLUDED!!!
(damn you, python)
Note that now, we can have a two-number outcome (that might help with the heads/or/tails program)
Predictably Random???
As you know, we like to test a program to verify that the decisions we are making are correct.
However, if the error / bug that we detected is only ocurring for some
random program states, we might not encounter those issues again until it is too late!
That's why there is a way to specify a
starting point for random that will create a repeatable pattern of random events.
We call this the
seed.
Specifying the random seed
In Python, you can specify a seed and see repeated random events like this:
import random
def main():
'''Using random-module functions'''
random.seed(123)
for i in range(30):
num = random.randint(4,13)
print(f"random number {i:2d} : {num:2d} ")
if __name__ == "__main__":
main()
You can try it out
here
Note you can pick any integer seed you like.
Read the details
here
What do we get? We still generate random numbers every time we call any of these functions, but, crucially, once we restart the program,
they are the same sequence of random numbers!
Odds and Ends
- Function annotations
- Syntax errors
- Exceptions
- Logical (semantic) errors
Function annotations
There is a way in python to indicate (annotate) the desired type for input arguments and the desired return type for functions.
The following is a function with the following characteristics:
- The function is called foo
- its first argument, called st should be a string
- its second argument, called num should be an integer
- it should return a string
- it should return a string that has the concatenation of the string st, num times, with no whitespaces (" ", "\n", and "\t").
def foo(st:str, num:int) -> str:
'''returns a string concatenated num times with no whitespaces'''
st = st.strip().replace(" ", "").replace("\t", "").replace("\n", "")
return st*num
def main():
'''Using a function with annotations'''
my_str = " Abra \n ca \t dabra "
out = foo(my_str,3)
print(out)
if __name__ == "__main__":
main()
You can try it
here
Syntax errors, vs Exceptions, vs Logical errors
We've encounteres several errors so far.
You should notice that when running python programs and encountering an error, we get messages that indicate either an syntax error or a series of exceptions.
Syntax Error
These arise when the program is not written using valid syntax. The usual problems are issing symbols or misspelled words.
import random
def main():
'''Using random-module functions'''
for i in range(30):
num = random.randint(4,13)
print(f"random number {i:2d} : {num:2d} )
if __name__ == "__main__":
main()
The output is:
Exceptions
Errors detected during code execution are called exceptions.
example 1: Divide by zero
def main():
num = 7 * 4 / 0
print (num)
if __name__ == "__main__":
main()
The output is:
example 2: missing variable (unknown term)
def main():
print ( my_str * 8)
if __name__ == "__main__":
main()
The output is:
example 3: exception on operation
def main():
print ( '2' + 2)
if __name__ == "__main__":
main()
The output is:
Here is a
List of Exceptions
The basic rule is: if it is properly written and you still get an error, it is probably an exception.
Logical Errors (semantic errors)
Therse arise from a mismatch between expected results and actual results.
This usually means that the programmed logic is NOT the logic we wanted the program to complete.
The following program was written in hopes of getting the following console printout for an input of
5:
0
0 1
0 1 2
0 1 2 3
0 1 2 3 4
def main():
num = int(input("give me an integer: "))
if num <= 0:
print ("negative")
if num >= 0:
print ("positive")
for i in range (num):
for j in range (i):
print (j, end = " ")
print()
if __name__ == "__main__":
main()
You can try it out
here
What are the bugs!?
Homework
[Due for everyone]
Homework 04 (Replit); due on 02/25 before 5 PM
[Optional] TODO
EXTRA
Recap: Chaining
Remember this problem?:
- Set variable my-Str = "⎵⎵he-lloWo-rl⎵d-!"
- Count the number of "tokens" in my-str where a "token" is any Substring Surrounded with either "⎵"or "-"
- return the uppercase first letter of the third token
Activity 1 [2 minutes]:
Show and Tell
Solution below:
def find_occurrence(phrase: str, sub: str, k: int) -> int:
'''Finds the k-th occurrence of sub inside phrase'''
n_spaces = phrase.count(sub)
idx = 0
for oc in range(1, n_spaces):
idx = phrase.find(sub, idx+1) #note the phrase is 'stripped'
if oc == k:
return idx
def main():
'''Prints the uppercase first letter of the THIRD token (⎵,-)'''
#phrase = " he-lloWo-rl d-!"
#phrase = " this is-another-example . "
phrase = input("Gimme a phrase with ' ' or '-' separators: ")
phrase = phrase.strip().replace('-'," ")
print(phrase)
#let = phrase.find(" ",???) # can't say "second occurrence"
ind_second_space = find_occurrence(phrase, " ", 2)
letter = phrase[ind_second_space+1].upper()
print(f"This uppercase first letter of the THIRD token is {letter}")
if __name__ == "__main__":
main()
Try the solution Here
Splitting and Joining Strings
Another typical task is to separate a string into a list of component substrings, or tokens.
Quick lookahead: lists
A list is a sequence of elements stored consecutively (like a train). The following is the assignment and printing of one:
You can try it
here
split
The
split method can achieve this by specifying the character(s) that are to be considered the
separators between the tokens.
We will learn how to work with lists in a future lecture.
join
Another useful task is to join tokens into a longer string.
The format is:
< separator > . join ( < list of tokens > )
We'll see these after we introduce lists.