PC204 Lecture 2

Tom Ferrin

tef@cgl.ucsf.edu

If you missed last week...

Homework Answers

  • 1.1 - Write a function to compute the volume of a sphere of radius 5
    def sphereVol(radius):
        import math
        vol = 4.0 / 3.0 * math.pi * radius ** 3
        # exponentiation has higher precedence than multiplication!
        return vol
    
    print(sphereVol(5))
    

     
  • 1.2 - Write some functions that calculate the remainder of two integers
    def method1(dividend, divisor):
        # use +, -, * and / operators
        quotient = dividend / divisor
        remainder = dividend - quotient * divisor
        return remainder
    
    def method2(dividend, divisor):
        # use divmod function
        quotient, remainder = divmod(dividend, divisor)
        return remainder
    
    def method3(dividend, divisor):
        # use % operator
        return dividend % divisor
    
    print(method1(42, 5))
    print(method2(42, 5))
    print(method3(42, 5))
    

Case Study #1: Interface Design


 
Most of what’s here is just from chapter 4 of Think Python 2e, but we’ll walk through the examples line-by-line in class.
 

Swampy Installation

  • The first thing to do is to install the Swampy Python package on your computer, available from pypi.python.org/pypi/swampy. And since this is a "pypi" package, you can just use the "pip" command to do this...
    /Users/tef> sudo pip3 install http://www.rbvi.ucsf.edu/Outreach/pc204/swampy-2.1.7.tar.gz
    Password:
    Collecting http://preview.rbvi.ucsf.edu/Outreach/pc204/swampy-2.1.7.tar.gz
      Downloading http://preview.rbvi.ucsf.edu/Outreach/pc204/swampy-2.1.7.tar.gz (49kB)
    Installing collected packages: swampy
      Running setup.py install for swampy ... done
    Successfully installed swampy-2.1.7
    
    I needed the "sudo" and associated password because the directory where the package of files gets installed is not writable by most users.

    If you get an error about the pip command "not found", just Google for "how to install pip" and follow the given directions.

Installation (continued)

  • On Windows, you have to start the Command Prompt:
    1. Go to search and type "command". The top hit should be "Command Prompt". Hitting Enter/Return or clicking on entry should start Command Prompt.
    2. Type
      "c:\Program Files\Python37\python.exe" -m pip install --user http://www.rbvi.ucsf.edu/Outreach/pc204/swampy-2.1.7.tar.gz
      to install. The double quotes are part of the command. If you are not using Python 3.7, you will need to change Python37 to match your Python version. The --user indicates that swampy should only be installed for the current user. If you wish to install it for all users of your computer, you will need to run the command prompt as administrator and omit --user from the command.

     
  • If you use Linux, or run into problems installing the Swampy package, there are detailed instructions available here.

Packages and Modules

  • A package is a collection of Python modules. We'll be using functions from the TurtleWorld module, and we'll do that by importing the TurtleWorld module from the swampy package using the Python "import" command:
    
    import swampy.TurtleWorld
    
    
  • I can then call functions in the TurtleWorld module. The first function I want to call is named "TurtleWorld." (It's okay to have functions that have the same name as modules, and in fact this is quite common.) I call the function like this...
    
    import swampy.TurtleWorld
    
    world = swampy.TurtleWorld.TurtleWorld()
    
    
  • Of course this approach could lead to a lot of typing if you have to make many function calls. Fortunately the import command allows you to define an abbreviated name for the module, like this...
    
    import swampy.TurtleWorld as tw
    
    world = tw.TurtleWorld()
    
    
  • Here's the first program I wrote, called demo1.py:
    import swampy.TurtleWorld as tw
    
    world = tw.TurtleWorld()   # create a new world
    bob = tw.Turtle()          # create a new turtle
    print(bob)		# print out what kind of thing "bob" is
    
    tw.fd(bob, 100)	# move bob forward 100 steps
    tw.rt(bob)	# turn bob to the right
    tw.fd(bob, 100)	# move forward 100 steps
    tw.rt(bob)	# turn right, and so on
    tw.fd(bob, 100)
    tw.rt(bob)
    tw.fd(bob, 100)
    tw.rt(bob)
    
    tw.wait_for_user()  # pause and wait for the user to close the window
    
  • And although we haven't covered iteration yet, I'll just tell you that it's easier to do the above with a "for" loop:
    import swampy.TurtleWorld as tw
    
    world = tw.TurtleWorld()
    bob = tw.Turtle()
    print(bob)
    
    for i in range(4):
        tw.fd(bob, 100)
        tw.rt(bob)
    
    tw.wait_for_user()
    
  • We can combine what we learned earlier about functions and "encapsulate" the code that draws the square:
    import swampy.TurtleWorld as tw
    
    def square(t, edgeLength):
        for i in range(4):
            tw.fd(t, edgeLength)
            tw.rt(t)
    
    world = tw.TurtleWorld()
    bob = tw.Turtle()
    square(bob, 100)    # call the "square" function
    tw.wait_for_user()
    
  • In fact, I can pretty easily "generalize" my function to draw a polygon with any number of sides:
    import swampy.TurtleWorld as tw
    
    def polygon(t, n, edgeLength):
        angle = 360.0 / n;
        for i in range(n):
            tw.fd(t, edgeLength)
            tw.rt(t, angle)
    
    world = tw.TurtleWorld()
    bob = tw.Turtle()
    polygon(bob, 15, 100)
    tw.wait_for_user()
    
  • But what if I want to be able to draw circles of a given radius too? One way to do this is to approximate a circle using a polygon with a large number of sides, say 100:
    def circle(t, radius):
        import math    # import the Python math module
        circumference = 2 * math.pi * radius
        numSides = 100
        edgeLength= circumference / numSides
        polygon(t, numSides, edgeLength)
    
  • The problem with this is that the number of sides is always 100, so that for very large circles the line segments are too long and for very small circles I waste a lot of time drawing very short line segments that you probably can't even see. A better approach would be to chose the number of sides for the polygon based on the size of the circle, say like this:
    def circle(t, radius):
        import math
        circumference = 2 * math.pi * radius
        numSides = int(circumference / 3) + 1
        edgeLength= circumference / numSides
        polygon(t, numSides, edgeLength)
    
    Now the number of sides is approximately 1/3 of the circumference, which produces reasonable nice looking circles regardless of size.
    (Why the "+1" in the numSides calculation? Hint: what if the radius is 0.477 or less?)
  • When you have a function with several arguments, it's easy to forget the right order they go in, or maybe even forget and leave one out altogether. Python provides a nice way to avoid this using keyword arguments:
    import swampy.TurtleWorld as tw
    
    def circle(t, radius):
        import math
        circumference = 2 * math.pi * radius
        numSides = int(circumference / 3) + 1
        edgeLength= circumference / numSides
        polygon(t, numSides, edgeLength)
    
    def polygon(t, n, edgeLength):
        angle = 360.0 / n;
        for i in range(n):
            tw.fd(t, edgeLength)
            tw.rt(t)
    
    world = tw.TurtleWorld()
    bob = tw.Turtle()
    bob.delay = 0.01    # draw faster
    polygon(bob, edgeLength=100, n=5)    # note use of parameter names
    wait_for_user()
    
  • Python also provides a convenient way to specify default values for function arguments:
    import swampy.TurtleWorld as tw
    
    def circle(t, radius):
        import math
        circumference = 2 * math.pi * radius
        numSides = int(circumference / 3) + 1
        edgeLength= circumference / numSides
        polygon(t, numSides, edgeLength)
    
    def polygon(t, n=4, edgeLength=50):    # note use of default parameters
        angle = 360.0 / n;
        for i in range(n):
            tw.fd(t, edgeLength)
            tw.rt(t)
    
    def move(x, y):
        tw.pu(bob)    # "pen up"
        tw.bk(bob, x) # back "x" units
        tw.lt(bob)
        tw.fd(bob, y) # up "y" units
        tw.rt(bob)
        tw.pd(bob)    # "pen down"
    
    world = tw.TurtleWorld()
    bob = tw.Turtle()
    bob.delay = 0.01
    polygon(bob, edgeLength=100, n=5)    # note use of parameter names
    move(150, 150)
    polygon(bob, edgeLength=50)    # note number of sides not given
    tw.wait_for_user()
    

     
  • And here's the documentation for all the functions implemented in the TurtleWorld module: TurtleWorld.html

    ThinkPython's Approach Now Considered Bad Form

  • Note that we have been doing import's like this...
    
    import swampy.TurtleWorld as tw
    
    
    But Chapter 4 of ThinkPython does this...
    
    from swampy.TurtleWorld import *
    
    
    Importing this way is now considered poor programming practice. The problem with this approach is that all of the functions in the TurtleWorld module become part of our "namespace." So if TurtleWorld happened to have a function called polygon() its definition would conflict with our definition of polygon(). The former approach to importing avoids this issue, since functions in the TurtleWorld module are always referenced as "tw.something".

Conditional Statements

  • A conditional or "if" statement selects actions to perform and, thus, allows you to selectively act on objects. Normally, Python statements execute one after the other, but "if" (and "for" and "while") statements cause the interpreter to jump around. An "if" statement may control a single action or a series of actions through use of compound statements.
  • Syntax:
    if test_expression1:
        statement_block_1    # these statements executed if test_expression evalutes to true
    elif test_expression2:
        statement_block_2    # if expression1 is false but expression2 is true
    else:
        statement_block_3    # if neither expression1 nor expression2 are true
    
  • Notes:
    1. The "elif" and "else" parts are optional.
    2. Each of the "test_expression" parts (the conditional tests) and the "else" part have an associated block of nested statements. A block of nested statements are statements that are indented from the header line.
    3. Don't forget the colon at the end of the header lines!

Comparison Operators


 
Operator Meaning
== Equal to
!= Not equal to
< Less than
> Greater than
>= Greater than or equal
<= Less than or equal

Examples

a = 42
if a == 42:
    print("a equals 42")

if a % 2 == 0:
    print("a is even")
else:
    print("a is odd")

s1 = "today is the first day"
s2 = "of the rest of your life"

if s1 != s2:
    print("the strings are different")

if s1 < s2:
    print("s1 is less than s2")

"test_expression" values

  • Recall the "test_expression" part of an "if" statement. It helps to know what's true and what's false.
    • All Python objects respond to comparison operators
    • Numbers are compared by relative magnitude
    • Strings are compared lexicographically
    • Compound objects (like lists, dictionaries and tuples) are compared by inspecting each part of the compound object until true or false can be determined
    • An empty object like an empty string (s="") or the special "None" object is false, while non-empty objects are true

     
    
    a = 9
    b = 12
    a<b, a==b, a>b, b>=a
    (True, False, False, True)
    
    

Compound Statements

  • All statements indented the same distance to the right belong to the same block of code, until the block is ended by a line with less indentation. (I.e. It's indentation that determines block boundaries.) Indentation can consist of any combination of spaces and tabs. But you must be consistent! Since they both show as blank space on your screen, it's often difficult to tell the difference between sequences of spaces and sequences of tabs. Code blocks can be nested.
  • Example:
    if a % 2 == 0:
        # here's one level of indentation
        y = 3.1415 * 2 / r
        if y > 360:
            # here's another level of indentation
        	print("y greater than 360")
        	y = y - 360
        # now we're back to the first level of indentation
        z = y / 4
    # now there's no indentation
    # and if the "if a % 2 == 0" wasn't true this is where execution would have jumped to
    

Recursion


 
  • When a function calls itself, it's known as a recursive function. Functions that call themselves can be very useful for some types of problems. The classic example is computing N! (N factorial):
    def factorial(n):
        if n == 1:
            return n
        else:
            return n * factorial(n-1)
    
    print(factorial(10))
    

     
  • The main idea behind recursion is that a problem is either (a) solved, or (b) solvable using the same algorithm after some simplification. In the example above, if n == 1 the problem is solved. If n != 1 then we apply the same algorithm over again using n-1.

How Does This Work?

    def factorial(n):
        if n == 1:
            return n
        else:
            return n * factorial(n-1)
    
    print(factorial(10))
    
  • The first time the function factorial is called, n = 10. Since the test expression in the if statement is false, the statement in the else part is executed, which calls the factorial function using the value 9. The test expression is still false, so the factorial function is called again, this time using a value of 8. This continues until the function is finally called with a value of 1. Now the test expression is finally true, in which case the function just returns the value 1. This return value is multiplied by the (previous) value of n, which was 2, and the result (2) is returned. This return value is multiplied by the previous value of n, which was 3, and the result (6) is returned. This process continues until finally the accumulated product is multiplied by the original value of n (10), and this result is returned to the original caller of the function and gets printed out. The answer is 3,628,800.
  • The key to understanding how this all works is knowing that Python keeps track of when functions are called using a "stack," with each function call represented by a "frame" on the stack. A convenient analogy is the spring loaded column of dishes that are used in some cafeterias. As you put a new dish on the top of the column, the dishes below are pushed further down. In our analogy, each dish is a frame and the column is the stack.
  • The stack starts out empty. The first time factorial is called, a dish with "10" written on it is put on the stack. Because the if statement is false, factorial is called again, and so another dish is put on the stack, this time with "9" (=10-1) written on it. Factorial is called again, and a plate labeled with "8" gets put on the stack, and so forth. Finally a plate with "1" written on it gets put on the stack. This time something different happens. Because the if statement is now true the "return n" statement is executed. This is equivalent to taking the top plate off the stack. The "1" written on that plate is the value returned by the function, and the code says to use this value and multiply it by the value saved on the next plate, which is 2. You then use that product and multiply it by the value written on the next plate (3). This continues until you take the accumulated product, multiply it by the value on the top plate, then take this plate off the stack and see that there are no more plates. At that point the factorial function returns to the place in the code it was first called from (as part of the print statement) and the accumulated product is printed out.
  • You can see the stack that Python creates by intentionally creating an error in you code...
    def factorial(n):
        if n == 1:
            return n / 0    # dividing by zero causes a run time error
        else:
            return n * factorial(n-1)
    
  • Now when you run the program you get...
    Traceback (most recent call last):
      File "factorial.py", line 7, in ?
        print(factorial(10))
      File "factorial.py", line 5, in factorial
        return factorial(n-1) * n
      File "factorial.py", line 5, in factorial
        return factorial(n-1) * n
      File "factorial.py", line 5, in factorial
        return factorial(n-1) * n
      File "factorial.py", line 5, in factorial
        return factorial(n-1) * n
      File "factorial.py", line 5, in factorial
        return factorial(n-1) * n
      File "factorial.py", line 5, in factorial
        return factorial(n-1) * n
      File "factorial.py", line 5, in factorial
        return factorial(n-1) * n
      File "factorial.py", line 5, in factorial
        return factorial(n-1) * n
      File "factorial.py", line 5, in factorial
        return factorial(n-1) * n
      File "factorial.py", line 3, in factorial
        n / 0
    ZeroDivisionError: integer division or modulo by zero
    
  • ...where each pair of lines is a frame in the Python stack.
  • What happens if you call the factorial function with the value 0?
    def factorial(n):
        if n == 1:
            return n
        else:
            return n * factorial(n-1)
    
    print(factorial(0))
    
  • Because the test expression in the if statement will never be true, the function will just keep calling itself forever. Or at least until the stack grows big enough to use up all the memory on your computer and an "out of memory" error occurs. So it's best to guard against this condition, and you may even remember that, by definition, the value of 0! is 1. So here’s a better version of the function:
    def factorial(n):
        if n <= 1:
            return 1
        else:
            return n * factorial(n-1)
    
  • Strictly speaking, this still isn’t completely correct because factorials are undefined for negative values of N. And also there’s no such thing as the factorial of a non-integer. So we need we need to add more checks in our function to guard against these incorrect uses by ourselves and others. So our complete function becomes...
    def factorial(n):
        """Return the factorial of integer n"""
        if type(n) != type(1):
            print("n must be an integer")
            return None
        if n < 0:
            print("factorial only defined for positive integers")
            return None
        if n <= 1:
            return 1
        else:
            return n * factorial(n-1)
    
  • The triple quoted text string example above is a "docstring." You can put any text you want in a docstring, but by convention it contains whatever essential information a programmer needs to know to be able to use the function. You can print the contents of a function’s docstring like this...
    
    print(factorial.__doc__)
    
    
  • Like all triple quoted strings, docstrings can be any number of lines long. So, thinking back to the beginning of today’s class,...
    /Users/tef> python
    Python 2.7.5 (default, Mar  9 2014, 22:15:05) 
    [GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.0.68)] on darwin
    Type "help", "copyright", "credits" or "license" for more information.
    >>>
    >>> import swampy.TurtleWorld as tw
    >>> print(tw.TurtleWorld.__doc__)
    An environment for Turtles and TurtleControls.
    >>>
    >>> print(tw.Turtle.__doc__)
    Represent a Turtle in a TurtleWorld.
       Attributes:
          x: position (inherited from Animal)
          y: position (inherited from Animal)
          r: radius of shell
          heading: what direction the turtle is facing, in degrees.  0 is east.
          pen: boolean, whether the pen is down
          color: string turtle color
    >>> ^D
    /Users/tef> 
    
  • Python also has a "help" command that can make use of both docstrings and something called "introspection" where Python examines all the functions in a module and then prints out information about these functions, their arguments, and their docstrings. For example, "help(tw.Turtle)" prints...
    
    Help on class Turtle in module swampy.TurtleWorld:
    
    class Turtle(swampy.World.Animal)
     |  Represents a Turtle in a TurtleWorld.
     |  
     |  Attributes:
     |      x: position (inherited from Animal)
     |      y: position (inherited from Animal)
     |      r: radius of shell
     |      heading: what direction the turtle is facing, in degrees.  0 is east.
     |      pen: boolean, whether the pen is down
     |      color: string turtle color
     |  
     |  Method resolution order:
     |      Turtle
     |      swampy.World.Animal
     |      __builtin__.object
     |  
     |  Methods defined here:
     |  
     |  __init__(self, world=None)
     |  
     |  bk(self, dist=1)
     |      Moves the turtle backward by the given distance.
     |  
     |  draw(self)
     |      Draws the turtle.
     |  
     |  fd(self, dist=1)
     |      Moves the turtle foward by the given distance.
     |  
     |  get_heading(self)
     |      Returns the current heading in degrees.  0 is east.
     |  
     |  get_x(self)
     |      Returns the current x coordinate.
     |  
     |  get_y(self)
     |      Returns the current y coordinate.
     |  
     |  lt(self, angle=90)
     |      Turns left by the given angle.
     |  
     |  pd(self)
     |      Puts the pen down (active).
     |  
     |  pu(self)
     |      Puts the pen up (inactive).
     |  
     |  rt(self, angle=90)
     |      Turns right by the given angle.
     |  
     |  set_color(self, color)
     |      Changes the color of the turtle.
     |      
     |      Note that changing the color attribute doesn't change the
     |      turtle on the canvas until redraw is invoked.  One way
     |      to address that would be to make color a property.
     |  
     |  set_pen_color(self, color)
     |      Changes the pen color of the turtle.
     |  
     |  step(self)
     |      Takes a step.
     |      
     |      Default step behavior is forward one pixel.
     |  
     |  ----------------------------------------------------------------------
     |  Methods inherited from swampy.World.Animal:
     |  
     |  die(self)
     |  
     |  lt(self, angle=90)
     |      Turns left by the given angle.
     |  
     |  pd(self)
     |      Puts the pen down (active).
     |  
     |  pu(self)
     |      Puts the pen up (inactive).
     |  
     |  rt(self, angle=90)
     |      Turns right by the given angle.
     |  
     |  set_color(self, color)
     |      Changes the color of the turtle.
     |      
     |      Note that changing the color attribute doesn't change the
     |      turtle on the canvas until redraw is invoked.  One way
     |      to address that would be to make color a property.
     |  
     |  set_pen_color(self, color)
     |      Changes the pen color of the turtle.
     |  
     |  step(self)
     |      Takes a step.
     |      
     |      Default step behavior is forward one pixel.
     |  
     |  ----------------------------------------------------------------------
     |  Methods inherited from swampy.World.Animal:
     |  
     |  die(self)
     |      Removes the animal from the world and undraws it.
     |  
     |  polar(self, x, y, r, theta)
     |      Converts polar coordinates to cartesian.
     |      
     |      Args:
     |          x, y: location of the origin
     |          r: radius
     |          theta: angle in degrees
     |      
     |      Returns:
     |          tuple of x, y coordinates
     |  
     |  redraw(self)
     |      Undraws and then redraws the animal.
     |  
     |  set_delay(self, delay)
     |      Sets delay for this animal's world.
     |      
     |      delay is made available as an animal attribute for backward
     |      compatibility; ideally it should be considered an attribute
     |      of the world, not an animal.
     |      
     |      Args:
     |          delay: float delay in seconds
     |  
     |  undraw(self)
     |      Undraws the animal.
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors inherited from swampy.World.Animal:
     |  
     |  __dict__
     |      dictionary for instance variables (if defined)
     |  
     |  __weakref__
     |      list of weak references to the object (if defined)
     |  
    
     |  delay
    
  • We'll learn more about introspection when we learn about object oriented programming and object "classes".

Last Words on Recursion

  • When should you think about using recursive functions? In general, any time something is defined recursively.
  • Recursive definitions come up in math fairly often, like the definition of factorial...
    0! = 1
    n! = n(n-1)!
    
    Or the definition of the fibonacci sequence of numbers...
    fibonacci(0) = 0
    fibonacci(1) = 1
    fibonacci(n) = fibonacci(n-1) + fibonacci(n-2)
    

     
  • Recursion comes up in a limited number of circumstances, to be sure, but when it does arise recursive functions are almost always the simplest way to code up the algorithm.

Homework

It should probably come as no surprise that this week's homework involves writing recursive functions...
 
  • 2.1 - Write a recursive function that prints a range of numbers
  • 2.2 - Implement Euclid's algorithm to find the greatest common divisor