Class 2

Variables and Functions

Objectives for today

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

Assignment

Everything we have done so far was very ephemeral. We gain tremendous power by being able to save and use intermediate results. We do so by assigning values (the result of a expression) to a variable.

The assignment syntax is <variable> = <expression>, e.g.,

x = 7

Nothing was printed by the REPL. So did anything happen? To figure it out, lets build up our mental model of what is going on in the Python interpreter.

Assignment semantics:

  1. Evaluate the right-hand side (RHS) of the expression (all expressions in Python produce a value, and thus have a type)
  2. Store the resulting value in memory
  3. Set the variable name on the left-hand side (LHS) to point to the memory location with result Unlike expressions, the assignment statement does not evaluate to a value and thus nothing is printed to the REPL.

We can create these kind of diagrams automatically with Python Tutor.

What’s in a name?

Python variable names can generally be of any length, contain upper and lower case letters, digits and the underscore character (_). Although variable names can contain digits, a variable name can’t start with a digit. Additionally there are a set of Python keywords (or “reserved” words) such as def that have specific meaning in the Python language and so can’t be used as variable names. As you might imagine variable naming can get very contentious. We will talk a lot more about good “style”, of which variable naming is an important aspect, but in the meantime, aim for concise, descriptive variables names (e.g. “income” instead of “i” or “asdf”) that minimize the cognitive burden anyone reading your code.

Thonny mechanics (saving files, and finding errors)

If we close and restart Thonny and try to use variables we created before, we will get errors

x
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 x

NameError: name 'x' is not defined

We could repeat everything that we did, e.g.

x = 4
x
4

but that is very inefficient and can’t possibly be the “way”,

If we want to save our code, and use that code to regenerate results, we will need to create additional files. We can do so using the text editor panel in Thonny.

# An example script
x = 7
x

Everything to the right of # (and including the #) is a comment and is not executed. It is an explanatory note for the reader (which will often be you!).

I will create a script with our command, save the file, and then run it (with the green arrow). The result is the similar to if we had typed the same code into the interpreter, except we don’t get line-by-line feedback. That is we are not executing each line in the context of REPL (with the “print”) but just “read” and “eval”. We will need to use explicit print functions to see the result.

# An example script
x = 7
print(x)
7

But we can use the variables we defined in the file later in the interpreter:

>>> x
7

Lets introduce an error so we see what Thonny reports, e.g. referencing an undefined variable y:

>>> %Run class2.py
Traceback (most recent call last):
  File "class2.py", line 3, in <module>
    y
NameError: name 'y' is not defined

Functions

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

x = 7
str(x + 7)
y = str(x + 7)
y
'14'
'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.

Recall our previous discussion about doing tasks many, many times.

Functions in Python (and other programming 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
4**2
9
16

We can implement the “squaring” with a function

Note

To end a function you are directly typing in the shell (interpreter), 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:

  • def is a keyword; it has a special meaning to Python, defining a function, and cannot be used as a variable or function name (there are approximately 30 such “reserved” keywords).
  • f is the function name (could be any valid identifier)
  • Everything inside the parentheses are the parameters, e.g. x. There can be zero or more parameters. These variables are only defined within the function body or scope.
  • I indent the body of the function, that is the code that the function will execute. This is how Python distinguishes the function body from other code.
  • The return statement immediately terminates the function, and determines the value returned to the caller. If there is no return, the function terminates when it reaches the bottom (and returns None, a special constant meaning the absence of a value). A function call evaluates to the function’s return value.

Now let’s invoke out newly defined function.

f(3)
f(4)
y = f(5)
y
f(4,5)
9
16
25
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[12], line 5
      3 y = f(5)
      4 y
----> 5 f(4,5)

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.

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?

# 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.

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.

patty_and_cheese = doubledouble - cheeseburger
cheese = cheeseburger - hamburger
patty = patty_and_cheese - cheese
# 100 x 100!
hamburger + 99 * patty + 100 * cheese
90.85
# 100 x 50 (100 patties and 50 cheeses)
hamburger + 99 * patty + 50 * cheese
78.35

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 (more recently a Double-Double was $3.45, a cheeseburger was $2.40 and a hamburger was $2.10)?

A function is a natural way to encapsulate the calculation in a reusable way. The parameters would be the prices of the 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

We could then test our function by recovering the prices for hamburger, cheeseburger, Double-Double as well as recomputing the combinations we did above, e.g.,

innout_price(1.50, 1.75, 2.65, 100, 50)
78.35