Lecture 7: Functions in Depth, Debugging

Topics

Lingering HW3 Questions

Presentation

How do functions really work?

  • Refresher on arrow diagrams.
  • Let's go back to simple.py.
    • We can walk through what happens when we make a call to add_then_double, e.g.

      >>> add_then_double(10, 20)
      
      1. The arguments to the function (10 and 20) are evaluated
        • they could have been more complicated expressions, e.g. add_then_double(5+5, 40/2)
      2. The function is then called and the parameters are associated with the values that are passed in
        • think of it like saying:

          a = 10
          b = 20
          
        • we now have two variables in the function that have the values of the information passed in
        • the only tricky thing is that these variables are unique to this function, so even if they're defined globally, they will be different than the global variables
      3. The function body for add_then_double is executed a statement at a time
        • This will in turn call add(a, b)
          • like any other function call we:
            • evaluate the arguments, in this case, we evaluated a, which gives us 10, and b, which gives us 20
            • we then pass these values to add
              • these values get associated with parameter names, num1 and num2
          • add is only one line of code, it adds the number and then returns that value
            • num1 + num2 gives us 30 (10 + 20)
          • the return statement means wherever this function was called, that is the value that call should evaluate to
            • the call add(a, b) will then represent the value 30
        • We then assign what add(a,b) returns to the variable added
        • double is called with what was in the variable added (30) and gives us back 60
        • 60 is then associated with the variable doubled
        • Finally, add_then_double returns doubled, which has the value 60
    • If we called add_then_double from the console, we will see the value 60 echoed, not because it was printed out but because that call (add_then_double(10, 20)) has been evaluated to the value 60
    • We could just have easily done other things with it, e.g.

      >>> add_then_double(10,20) * 2
      120
      >>> result = add_then_double(10,20)
      >>> result
      60
      >>> add_then_double(add_then_double(10, 20), 4)
      128
      
  • write a method called contains that takes a list and an item and returns True if the item is in the list, False otherwise, i.e.

    def contains(some_list, value):
        # ...
    
    • Look at the first option in contains.py
      • loops through each value in the list
        • if it finds one that is equal to the item we're looking for, then return True
          • remember that return immediately exits the function
          • this will stop the loop and then give back True from the function
        • if we make it through the entire list without finding one that's equal (and therefore exiting the function) then the loop will finish and we'll return False
          • this is similar to the isprime function we saw before in while.py
    • the contains_alternative function in contains.py code is another way of accomplishing this
      • we use a variable to keep track of whether or not we've found the item
        • initially, we set it to False
        • we then loop through all of the values
          • if we find the item we're looking for, we set the variable to True
          • not that this could happen multiple times if the item occurs multiple times, but it's fine to set it to True again if it's already set
        • after the loop finishes, we return found
          • if we didn't find any, it will still be False
          • otherwise, it will have been set to True

parameter passing and references

  • some terminology:
    • when we define a function's parameters we are defining the "formal parameters"

      def my_function(a, b):
         return a+b
      
      • a and b are formal parameters
      • until the function is actually called, they don't represent actual values
    • when we call a function, the values that we give to the function are called the "actual parameters" (sometimes also called the arguments)

      >>> x = 10
      >>> y = 20
      >>> my_function(x, y)
      30
      
      • the values 10 and 20 are the actual parameters (or similarly, x and y become the actual parameters)
  • when a function is called the following happens:
    • the values of the actual parameters are determined (we evaluate the expression representing each parameter)
      • the value is just an object (could be an int, float, string, etc)
    • these values are then "bound" or assigned to the formal parameters
      • it's very similar to assignment
      • the formal parameters represent new variables
      • the formal parameters will then REFERENCE the same objects as those passed in through the actual parameters
  • let's consider the picture for our function above
    • x and y are both references to int objects
    • when we call my_function, the formal parameters a and b, will represent two new variables
    • these variables will reference the same thing as their actual parameters
      • think of it like running the statements:

        a = x
        b = y
        
        • a will reference the same thing as x
        • b will reference the same thing as y
  • for non-mutable objects, this whole story doesn't really matter. Why?
    • they could be references to the same thing or copies, if we can't mutate the object, the behavior is the same
  • how does this change for mutable objects?
  • look at the changer function in parameter_passing.py code

    >>> x = [1, 2, 3, 4, 5]
    >>> changer(x)
    
    • what does the picture look like for this function call?
      • x was a variable that references a list object
      • when the function was called, the formal parameter a will represent a new variable
      • a will reference the same thing as x
        • think of it like running the statement a = x
    • because the object is mutable and since the formal parameter references the same object as what was passed in, changes made to the object referenced by a will also be seen in x
  • notice, however, that operations that do not change/mutate the object will NOT be seen outside the function
  • look at the no_changer function in parameter_passing.py

    >>> x = [1, 2, 3, 4, 5]
    >>> no_changer(x)
    
    • in this case, we're assigning a to some new object
      • we can't change what x references!
      • x and a will no longer reference the same object
      • any changes to a after this will not affect x
  • why is variable assignment and parameter passing done based on the references (i.e. a shallow copy) rather than a deep copy of the whole object?
    • sometimes you want to modify the object passed in
    • performance (it takes work to copy the object)
      • often we just want the value and don't need to mutate the underlying object
      • in some programming languages (Rust, C, C++) your parameters annotate whether they are by reference or by value
      • in some programming languages (Rust) your parameters annotate whether the function is allowed to mutate objects passed in by reference

bool variables

  • just like any other variables except it's of type bool
    • we've used variables to store ints, floats and strings
    • this works the same way
  • for example

    >>> x = True
    >>> x
    True
    >>> x = 10 < 0
    >>> x
    False
    
    • we need some way of keeping track whether or not the user has guessed correctly or not
    • we can us a bool variable and initially set it to some value
    • condition the while loop on this variable
      • change the value when we get it correct
  • in (we wrote contains just before!)
    • python actually has the contains functionality built-in

      >>> 1 in [1, 2, 3]
      True
      >>> 2 in [1, 2, 3]
      True
      >>> 5 in [1, 2, 3]
      False
      >>> "banana" in [1, 2, 3]
      False
      
    • the item has to be in the list as one of the individual items, e.g. the following doesn't work:

      >>> [1, 2] in [1, 2, 3]
      False
      
      but this would:
      >>> [1, 2] in [[1, 2], 2, 3]
      True
      
    • in works for other sequences like strings (does a character exist in a string) or tuples:

      >>> "a" in "banana"
      True
      
    • For strings, you can actually ask if a substring is in a string

      >>> "ana" in "banana"
      True
      
  • run number_guessing_game in number_game.py
    • picks a random number between 1 and 20 and you try and guess it
      • keeps prompting you until you get it right
      • gives you hints as to whether you need to guess higher or lower
    • how could we implement this?
      • pick a random number
      • as long as (while) the user hasn't guessed the right answer
        • get the guess from the user
        • if it's the right answer
          • print out "Good job!"
          • somehow indicate that we're done looping
        • otherwise, if the guess is too low
          • print out higher
        • otherwise (i.e. the guess must be too high)
          • print out lower
  • look at number_guessing_game function in number_game.py
    • use a variable called finished and initially set it to False
      • could have also use a variable like stillguessing and set it to True and then had while stillguessing:
    • when they get the number right we set finished = True and we will therefore exit the loop
    • notice there are other ways of writing this function, e.g. number_guessing_game2

A few more sequence operators

  • We've seen + for appending strings

    >>> "banana" + " split"
    "banana split"
    
    • it also works on other sequences

      >>> [1, 2, 3] + [4, 5]
      [1, 2, 3, 4, 5]
      >>> (1, 2, 3) + (4, 5)
      (1, 2, 3, 4, 5)
      
    • What do you think times does?

      >>> "*" * 10
      '**********'
      >>> "I'm " + "very "* 4 + "excited"
      "I'm very very very very excited"
      >>> [0] * 10
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
      >>> [1, 2, 3] * 2
      [1, 2, 3, 1, 2, 3]
      

scope.py

  • When we run the file, the first two functions will get defined
    • Then, it will execute the three statements at the end of the file
    • The first print statement will print out 20
    • The second print statement will print out 30
      • Why not 60? I.e., in double_input, we assigned 20 to val as the first line.
        • val = 2*val updates the value of the parameter NOT the variable outside the function
  • The "scope" of a variable is the portion of the code we can reference that variable in and have it be valid
    • Scope diagrams
    • When we declare a variable in the interpreter, what is its scope?

      >>> x = 10
      
      • the scope is any shell statements/expressions typed after it
    • When we declare a variable (outside a function) in a file, what is its scope?
      • anywhere below it in the file
        • remember, running a program is very similar to typing those commands into the shell
    • When we declare a variable inside a function, what is its scope?
      • anywhere below it in the FUNCTION
    • What is the scope of the parameters?
      • anywhere in the FUNCTION
      • it doesn't really make sense outside of the function since we wouldn't have a value for it
    • Additionally, the scope also defines the context for a variable reference.
  • What would be printed out if we ran scope2.py?
    • the program starts at the top and declares three "global" variables x, y and z
      • then it declares two functions
      • then it calls mystery1 with 10 and 20
        • the parameter a gets the value 10
        • the parameter z gets the value 20
          • notice that this is different than the global variable z!
          • in particular, when we execute z = 100, this reassigns the value of the local z, but not the global
        • The program prints out s, x, y and z
          • s is a local variable
          • x and y are global
          • z is the parameter
        • and then sets the value of the global variable z to 100
      • After mystery1 returns we print out the values of the global variables x, y and z
        • all three are unchanged!
          • x and y weren't changed anywhere
          • z was changed in mystery1, but the z being changed was the parameter, not the global z
    • What would happen if we added a call to mystery2 at the end and ran scope2.py?
      • Error. The scope of a is only defined within mystery1, so it cannot be accessed anywhere outside the function.
  • Why do programming languages limit a variable's scope?

Author: Joseph C. Osborn

Created: 2020-04-21 Tue 10:44

Validate