If we close and re-open Thonny and try to use variables we created last time, we will get errors
>>> x
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
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 use the text editor panel in Thonny.
# An example script
x = 7
x
The line beginning with a ‘#’ is a comment and is not executed. It is a 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)
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, say by referencing an
undefined variable y
:
>>> %Run lecture1.py
Traceback (most recent call last):
File "/Users/michaellinderman/Dropbox/Courses/MiddSIP-Summer2018/lectures/lecture1.py", line 3, in <module>
print(y)
NameError: name 'y' is not defined
Last time we actually used a function str
. And we just
used print
, another function. These are “built” into Python.
>>> str(x + 7)
'14'
What happens when you call or invoke a function like str
. Python:
Built in functions are just the start.
Using, and creating your own functions is a critical programming skill. Functions help us organize our code, and encapsulate computations for reuse with different inputs (think about our discussion on Day 1 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:
Let’s recreate that functionality (and function) in Python:
>>> 3**2
9
>>> 4**2
16
I can implement the “squaring” with a function.
>>> 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 perform the computation. To do so we will need to
invoke/call the function as we did above.
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 ~30 such keywords).f
is the function name (could be any unused valid identifier)x
. Can be zero
or more. These variables are only defined within the function body or scope.None
, a special constant meaning the
absence of a value).Now let’s invoke out newly defined function.
>>> f(3)
9
>>> f(4)
16
>>> 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 for that call and any local variables defined within the function.
This 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)
Write 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'
Write 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 yearly interest as
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(prinicpal, 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 myself 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 (local variables are stored in frame).
And even some more function examples.
So far we have largely glossed over the difference between print
and
return
. Consider:
>>> def double_it(x):
... return x * 2
...
>>> def another_double_it(x):
... print(x * 2)
...
when run in the interpreter, both functions produce the same output
>>> double_it(12)
24
>>> another_double_it(12)
24
but they are not the same. What if instead we assign the result to a variable?
>>> y = double_it(12)
>>> print(y)
24
>>> y = another_double_it(12)
24
>>> print(y)
None
Without the return
statement the value returned by the function to the caller
is the special constant None
which we briefly discussed previously.
Remember that when you type an expression into the console (i.e., at the
“»>” prompt), the interpreter evaluates the expression and prints the result.
The interpreter is effectively wrapping each expression with an implicit
print
; that is not the case when running a script.
Most of the time you will be using return
, because most of your functions
will intended for use as part of larger scripts or applications that will
consume the value produced by the function. And you don’t wan’t random values
printing all the time…
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 grade. Code should be designed to be read not just executed!
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.
In your lab we asked you to comment your code. The purpose of the comments is to describe what your code is doing, 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.
We will generally encounter two kinds of comments:
The former is what we have used so far. Comments, starting with a #
, included
in the code itself. Everything after 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.
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 how the function behaves
"""
return x * 2
Docstrings are a formal part of the Python specification. We will use the three
double quotes format, i.e., """
(docstrings are a special kind of multi-line
comment). 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 want to understand what you were trying to do with your code.
The docstring informs the output the help functionality. As you saw in the
reading 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
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!