print
, input
, str
, int
, etc.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:
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:
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 keywords).f
is the function name (could be any valid identifier)x
. Can be zero
or more. These variables are only defined within the function body or scope.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)
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)
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'
Define the type contract, i.e. the types of parameters the function accepts and the type it returns, e.g.
(str, str) -> str
Write the header
def glue_strings(first, second):
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.
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”).
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!
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:
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.
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)?
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