SmithLogo

CSC 111

Introduction to Computer Science Through Programming

Smith Computer Science



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:

  1. importing the module (from a known location)
  2. 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:
  1. using a common python module (like math or random (which are located in a known directory)

  2. or
  3. 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:

from math import *


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:

    1. Uniform distribution: all possible outcomes are equally likely
      Examples: fair coin tosses, fair dice rolls.
      See an example here

    2. 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:

    import random
    


    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

    1. Function annotations
    2. Syntax errors
    3. Exceptions
    4. 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:

    1. The function is called foo
    2. its first argument, called st should be a string
    3. its second argument, called num should be an integer
    4. it should return a string
    5. 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?:

    1. Set variable my-Str = "⎵⎵he-lloWo-rl⎵d-!"
    2. Count the number of "tokens" in my-str where a "token" is any Substring Surrounded with either "⎵"or "-"
    3. return the uppercase first letter of the third token



    Activity 1 [2 minutes]:
    Show and Tell


    Solution below:
    Enter the password to proceed:






    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.