Class 3

Objectives for today

  1. Given a function definition, identify the name, function parameters and body
  2. Distinguish between global variables, function parameters and local variables
  3. Determine the return value of a function
  4. Use built-in functions such as print, input, str, int, etc.
  5. Implement simple functions
  6. Describe aspects of good coding style

Functions

We have already used several functions, including print and str. These are “built” into Python.

>>> x = 7
>>> str(x + 7)
'14'
>>> y = str(x + 7)
>>> y
'14'

What happens when you call or invoke a function like str. Python:

  1. Evaluates all the arguments, the expressions within the parentheses, from left to right
  2. Sets the function parameters to those values
  3. Then executes the body of the function, with the function call evaluating to the return value of the function (in the example, above, the return value is either printed by the REPL or assigned to the variable y)

Built in functions are just the start.

Creating your own functions is a critical programming skill. Functions help us organize our code, and encapsulate computations (of any complexity) for reuse with different inputs (think about our previous discussion about doing tasks many, many times).

Functions on Python (and other languages) are similar to their mathematical counterparts (which associates values of one set, the inputs, to another set, the outputs). For example you likely have seen this notation for functions:

\(f(x)=x^2\)

Let’s recreate that functionality (and function) in Python. ** is the power operator in Python.

>>> 3**2
9
>>> 4**2
16

We can implement the “squaring” with a function (to end the function, hit enter on a blank line).

>>> def f(x):
    return x**2

I have now defined a function named f. Note that nothing was executed. A function definition is just that, the definition of the computation a function should perform. It does not actually perform the computation. To do so we will need to invoke/call the function as we did above with str.

Lets break down the function definition first:

Now let’s invoke out newly defined function.

>>> f(3)
9
>>> f(4)
16
>>> y = f(5)
>>> y
25
>>> f(4,5)
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
TypeError: f() takes 1 positional argument but 2 were given

Why did we get an error above? We tried to call f with more parameters than specified in the function definition.

What is the current value of x?

>>> x
7

Surprised? Note that parameters and any variables defined inside the function scope don’t change variables in the enclosing scope. That is the function definition creates a new scope. A variable’s scope is the area of a program in which its value can be accessed. A function invocation creates a new frame (which the PP book calls a “namespace”) that contains instances of the function parameters and any local variables defined within the function.

This is a good spot for us to pause and formalize our mental model of how memory works in Python for assignment, re-assignment and functions. Let’s use a combination of recent examples. Check out the “squaring” function in Python Tutor.

PI questions(credit)

Function writing: A recipe

  1. Create a specification of the function along some examples of the function call, e.g.

    “Return the string composed of first parameter, a space, and the second parameter”

    >>> glue_strings('John', 'Smith')
    'John Smith'
    
  2. Define the type contract, i.e. the types of parameters the function accepts and the type it returns, e.g.

    (str, str) -> str

  3. Write the header

    def glue_strings(first, second):
    
  4. Implement the body

    def glue_strings(first, second):
        return first + " " + second
    

Here is a more complex example for compound interest. Recall we can compute the value of savings earning interest yearly as

\(f(P, r, t) = P(1 + r)^t\)

where P is the principal, r is the rate and t is the time in years. How could we implement this in Python? (Show code)

# Calculates investment value with interest compounded yearly
def compound_interest(principal, rate, years):
    amount = principal * ((1 + rate) ** years)
    return amount

Note the function body can be multiple lines. The comment (beginning with #) is used to document to readers of the code (e.g., myself in the future) what the function does. Here we also define a “local variable” amount. Much like parameters, local variables exist only within the scope of the function body.

And here are some more simple function examples.

Good coding practice

A little about style

We should always aim for well-chosen, that is self-documenting, names for functions, variables, etc., and informative (but pithy) comments. The quality of your coding style will also be a factor in your programming assignment grades. Code should be written to be read not just executed! And the most likely person to read your code is “future you”, so think of good style as being kind to future you.

There is no one definition of “good style”, but in general it is combination of efficiency, readability and maintainability (that is can your code be readily updated/enhanced by you or others).

We will discuss style a lot more as we go, but it is never to early to develop good habits (and unlearn bad ones). In general we will follow the PEP 8 style guide. As a starting point we use “lowercase_words_separated_with_underscores” formatting for our variable and function names (sometimes called “snake case”).

Being DRY

You will often hear the acronym ‘DRY’ for Don’t Repeat Yourself, often as used as a verb. For example, “DRY it up”. The goal is eliminate duplicated code. Why? Duplicated code makes it harder to read and to update your code (to correct a mistake you might have to change a computation in many places!). Often the way to DRY it up is encapsulate duplicated code as a function!

Comments

In your programming assignments we will ask you to comment your code. The purpose of the comments is to describe the “why”, not to describe the code itself; you can assume the reader knows Python. But the reader doesn’t necessarily know what you are trying to do and why.

We will generally encounter two kinds of comments:

  1. Block and inline comments
  2. Docstrings

The former is what we have used so far, that is comments, starting with a #, included in the code itself. Everything after and including the # on the line will not be executed by Python. The second is a structured mechanism for documenting function, classes and other entities in your Python code that we will learn more about over the semester. Here is an example docstring for a function (a bad example):

def foo(x):
    """
    this is my docstring
    it can take up multiple lines
    this is a horrible docstring because it doesn't describe what the function does
    """
    return x * 2

Docstrings are a formal part of the Python specification. We will use the three double quotes format, i.e., """. If the function is very simple we can use a single line. If not, we want to document the purpose, parameters, return value and any exceptions.

def foo(x):
    """Doubles input.
    
    Args:
    	x: value to be doubled
    
    Returns:
    	Doubled value
    """
    return x * 2

What is the difference between docstrings and inline comments? The intended audience.

A docstring is intended for those who want to use your function, so it should describe what the function does, its parameters, and what (if any) return value is produces.

An inline comment is intended for those who are reading your code and need to understand what you were trying to do.

The docstring informs the output of the help functionality. You can obtain the documentation for functions, etc. with help. For example:

>>> help(print)
Help on built-in function print in module builtins:

print(...)
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
    
    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    file:  a file-like object (stream); defaults to the current sys.stdout.
    sep:   string inserted between values, default a space.
    end:   string appended after the last value, default a newline.
    flush: whether to forcibly flush the stream.

or

def double_it(x):
    """Double the input"""
    return 2*x

>>> help(double_it)
Help on function double_it in module __main__:

double_it(x)
    Double the input

Remember that the most likely person to read your code is you, in the future, after you forgot all about it. So think of good commenting as being kind to future you.

Putting it together: In-n-Out menu

In-n-Out is a fast food hamburger chain in the western US (mostly California). In-n-Out has an extensive “secret menu” (my order is always a Double-Double animal style), which used to include the option to order burgers with an arbitrary number of patties and cheese slices, e.g. a 3×3 with 3 patties and 3 slices of cheese, a 4×4 with four of each and so on.

In 2004, when I lived in California, an In-n-Out hamburger (no cheese) was $1.50, a cheeseburger (one patty, one slice of cheese) was $1.75 and a Double-Double (two patties and two cheese slices) was $2.65. In-n-Out pricing is linear; each additional patty and slice of cheese costs the same fixed amount. Let’s use the information above to compute the price in 2004 of the infamous 100×100 (yes it was a real thing) and a 100×50 (100 patties but only 50 cheese slices). Remember that a 100×100 still only has one bun and set of vegetable toppings.

Let’s start with the initial information, defined as set of Python variables with informative names:

hamburger = 1.50
cheeseburger = 1.75
doubledouble = 2.65

How can we infer the prices of the various components (as a hint, what is the difference between a cheeseburger and a hamburger)? And then use those prices to compute the cost of the 100×100 and a 100×50. Show the code…

patty_and_cheese = doubledouble - cheeseburger
cheese = cheeseburger - hamburger
patty = patty_and_cheese - cheese

price_100x100 = hamburger + 99 * patty + 100 * cheese 
price_100x50 = hamburger + 99 * patty + 50 * cheese

Note that you may encounter decimal values that are close, but not exact, e.g. 0.449999999999999 instead of 0.45; that is OK (it is a result in limitations on how Python represents floating point numbers, and something we will learn more about during the semester).

The above calculations only work for 2004 prices. How could we adapt our code to be able to compute the price for any number of patties and cheese slices, for any set of prices (currently a Double-Double is $3.45, a cheeseburger is $2.40 and a hamburger is $2.10)?

Show the answer and code…

A function is a natural way to encapsulate the calculation in a reusable way. The parameters would be the prices of a hamburger, cheeseburger and Double-Double, and the number of patties and the number of cheese slices you want to compute the price for. In keeping with our recipe above, an example would be

>>> innout_price(1.50, 1.75, 2.65, 100, 100)
90.85

and the type signature would be (float, float, float, int, int) -> float.

The full function, complete with docstring would be:

def innout_price(hamburger, cheeseburger, doubledouble, patties, cheeses):
    """Compute the price of an In-n-Out burger with arbitrary numbers of patties and slices of cheeses

    Args:
        hamburger: Price of a hamburger
        cheeseburger: Price of a cheeseburger
        doubledouble: Price of a Double-Double
        patties: Number of patties (must be at least 1)
        cheeses: Number of cheese slices (can be zero or more)
    
    Returns:
        Price of burger
    """
    patty_and_cheese = doubledouble - cheeseburger
    cheese = cheeseburger - hamburger
    patty = patty_and_cheese - cheese
    return hamburger + (patties - 1) * patty + cheeses * cheese