def double_it(x):
return x * 2
def another_double_it(x):
print(x * 2)
Class 3
Style, modules and loops
Objectives for today
- Distinguish between the return value of a function and its side-effects
- Use comments and docstrings to explicate code
- Obtain the documentation for functions and other Python objects
- Describe aspects of good coding style
- Explain the purpose of modules, and use standard modules like math
- Introduce the
turtle
module
Print vs. Return
So far we have largely glossed over the difference between print
and return
. Consider:
when run in the Shell, both functions produce the same output
12)
double_it(12) another_double_it(
24
24
but they are not the same. What if instead we assign the result to a variable?
That is more printing than we might have expected. The first 24 is the result of the print(y)
on line 2. The second is from the print(x * 2)
in the body of another_double it
. The None
is from the print(y)
on line 4. Without the return
statement the value returned by the function to the caller is the special constant None
which we briefly discussed before. That is Python implicitly adds a return None
to the end of the function. As such, on line 3 y
is assigned None
, which is printed on line 4.
Remember that when you type an expression into the shell/interpreter (i.e., at the “>>>” prompt), the interpreter evaluates the expression and prints the result (the ‘P’ in REPL). The interpreter is effectively wrapping each expression with an implicit print
; that is not the case when running a program you wrote in the editor.
Most of the time you will be using return
, because most of your functions will intended for use as part of larger programs that will consume the value produced by the function. And you don’t want random values printing all the time… However, in PA1 you will purposely implement functions that print
.
More generally, we distinguish between functions that have “side effects”, like printing to the screen, and those that do not. A function with side effects alters the state of the computer when executed (instead of or in addition to returning a value).
Good coding practice
A little about style
Our code must be correct, but we should also aim for good “style”. The latter reflects that the code we write must be readable by humans as well as computers. That is 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 a combination of efficiency, readability and maintainability (that is can your code be readily updated/enhanced by you or others). For formally, in the context of our programming assignments we evaluate style based on the following questions:
- Is the code written clearly, efficiently, elegantly and logically?
- Is the code readable, e.g. good use of whitespace, effectively commented, meaningful variable names, uses language style conventions?
We will discuss style a lot more as we go, but it is never too early to develop good habits (and unlearn bad ones). In general we will follow the PEP 8 style guide. As a starting point, focus on choosing informative variable names, using “lowercase_words_separated_with_underscores” formatting for variable and function names (sometimes called “snake case”), and thoughtfully using whitespace (e.g., blank lines) to separate functions and distinct code blocks within a function.
Being DRY
You will often hear the acronym ‘DRY’ for Don’t Repeat Yourself, often used as a verb. For example, “DRY it up”. The goal is to 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.
Constants
In Lab 1 we debugged a euclidean_distance
function. But what if we want to think about distance a bit differently? For example, maybe we want to express distance in light years. For reference, one light year refers to the distance it takes light to travel in one complete calendar year (365.25 days). To do this calculation, we need to know the speed of light.
The speed of light is a universal constant, and let’s start by using the value 300000000
meters per second. Our light_years
function might then look like, assuming distance
is given in meters.
def light_years(distance):
= distance / 300000000
seconds return seconds / (365.25 * 24 * 60 * 60)
What is 300000000
? Does that change? Furthermore, the 365.25
, 24
and the 60
all look like “magic” numbers. It’s bad practice to perform calculations in this way because (1) we might want to correct these numbers and (2) it’s not clear what these numbers mean. Code is not just for computers, it’s also for humans! All of these numbers are constants, so let’s represent them with a variable.
= 300000000
SPEED_OF_LIGHT = 365.25
DAYS_PER_YEAR = 24
HOURS_PER_DAY = 3600 # we could also expand this with SECONDS_PER_MINUTE and MINUTES_PER_HOUR
SECONDS_PER_HOUR
def light_years(distance):
= distance / SPEED_OF_LIGHT
seconds return seconds / (DAYS_PER_YEAR * HOURS_PER_DAY * SECONDS_PER_HOUR)
Note, we use all capitals to indicate constants.
Back to problem #1: note that SPEED_OF_LIGHT
is an approximation. We really should use:
= 299792458 SPEED_OF_LIGHT
If we didn’t use a constant, we would need to change every use of 300000000
in the code, which is not fun.
Let’s test our function by computing how many light years it takes for a ray of sunshine to reach us. The Sun is 149.6 million kilometers away, so the number of light years is:
149600000000) light_years(
which gives 1.5812732477008247e-05
(this is scientific notation, which refers to \(\approx 1.58 \cdot 10^{-5}\)). This is a very small number and not very intuitive to interpret. Maybe we can refactor our functions so we have both a light_minutes
and a light_years
function:
= 299792458
SPEED_OF_LIGHT = 365.25
DAYS_PER_YEAR = 24
HOURS_PER_DAY = 3600 # we could also expand this with SECONDS_PER_MINUTE and MINUTES_PER_HOUR
SECONDS_PER_HOUR = 60
SECONDS_PER_MINUTE
def light_minutes(distance):
= distance / SPEED_OF_LIGHT
seconds = seconds / SECONDS_PER_MINUTE
minutes return minutes
def light_years(distance):
= distance / SPEED_OF_LIGHT
seconds return seconds / (DAYS_PER_YEAR * HOURS_PER_DAY * SECONDS_PER_HOUR)
Hmm. There are some repetitive calculations here. Let’s DRY it up!
= 299792458
SPEED_OF_LIGHT = 365.25
DAYS_PER_YEAR = 24
HOURS_PER_DAY = 60
SECONDS_PER_MINUTE = 60
MINUTES_PER_HOUR
def light_minutes(distance):
= distance / SPEED_OF_LIGHT
seconds = seconds / SECONDS_PER_MINUTE
minutes return minutes
def light_years(distance):
= light_minutes(distance)
minutes return minutes / (DAYS_PER_YEAR * HOURS_PER_DAY * MINUTES_PER_HOUR)
Calling light_minutes(149600000000)
gives about 8.3 minutes. It takes over 8 minutes for sunshine to reach us!
In general we use constants to improve readability and to ease making changes. Keep this in mind since future assignments will contain places where you can and should be using constants.
Note that Python doesn’t enforce (like some other languages do) that SPEED_OF_LIGHT
is actually constant. Instead it is a community convention. You can change the value, but the Python community agrees you shouldn’t.
Organization of our files
Throughout the semester, we will try to organize our files in the same order. This makes it easier for us (and other programmers) to read our code because they know exactly where to look for module imports, constants, etc.
In general we aim for:
- A Docstring describing the file (in our case name, section, assignment, etc.)
- Module imports
- Constants
- Functions
Modules
Consider the function we debugged in Lab 1 in which we want to calculate the distance between two points. Let’s start by converting this calculation into a function:
def euclidean_distance(x1, y1, x2, y2):
= x2 - x1
deltaX = y2 - y1
deltaY return (deltaX ** 2 + deltaY ** 2) ** (0.5)
We used the square and square root directly with the power operator (**
). This is a little unclear. We could replace those with functions, e.g. write a square
and sqrt
functions. For example something like the following:
def euclidean_distance(x1, y1, x2, y2):
def sqrt(val):
return val ** 0.5
= x2 - x1
deltaX = y2 - y1
deltaY return sqrt(deltaX ** 2 + deltaY ** 2)
Is this allowed in Python? Yes. We would describe this as a nested function. Is it a good idea? Not really. Square root is a very common operation. We could imagine we might want to use this function in other settings. But since we defined it inside euclidean_distance
, it exists only in that scope and so is not available for use elsewhere. More generally, nested functions are used in very specific contexts (when we need to create a “closure”) that won’t occur in our class. Using more complicated features than needed increases the cognitive burden for anyone reading our code because they are left wondering if there is something more complicated going on that they missed. A part of good style is not making things more complicated than needed.
We would approach this instead as:
def sqrt(val):
return val ** 0.5
def euclidean_distance(x1, y1, x2, y2):
= x2 - x1
deltaX = y2 - y1
deltaY return sqrt(deltaX ** 2 + deltaY ** 2)
But we can do even better. As you might imagine, since square root and squaring are common operations, someone has already implemented them and we don’t need to reinvent the wheel. We can import the relevant functions from the math
module. For example,
import math
def euclidean_distance(x1, y1, x2, y2):
= x2 - x1
deltaX = y2 - y1
deltaY return math.sqrt(math.pow(deltaX, 2) + math.pow(deltaY, 2))
Python has whole module
s of functionality that can be import
ed and reused (Python’s tag line is “Batteries included”). You can import these modules several different ways
# Import functions etc. with `math.` prefix
import math as math
# Shorthand for import math as math
import math
# Import all functions directly into current symbol table, i.e. with no prefix
from math import *
# Import specific function
from math import pow, sqrt
Why might you choose one approach over the other? It is trade-off between convenience (typing shorter names) and the possibility of naming conflicts. By using the namespace prefix we prevent namespace conflicts, but may have to type more…
We will make extensive use of modules throughout the semester. One example is the random
module and specifically the randint
function.
from random import randint
help(randint)
Help on method randint in module random:
randint(a, b) method of random.Random instance
Return random integer in range [a, b], including both end points.
So if we wanted to choose a random angle to turn, specified in degrees, say while making a drawing we would do what?
0, 359) randint(
350
Why not 360? Recall randint
is inclusive and 0 is the same as 360. We would slightly oversample not making any turn at all.
An aside: Can computers generate truly random numbers? No. Because the algorithm (and the computer) are deterministic. In Python the random numbers are “pseudo random” numbers. Internally Python “seeds” its pseudo-random number generator with a seed and then generates a sequence of numbers based on that seed that are sampled from some distribution, typically a uniform distribution.
The implication is that if you know the seed and the algorithm you can predict the sequence of numbers.
This can actually be really critical for debugging. So languages typically allow you to set the seed.
from random import seed
help(seed)
Help on method seed in module random:
seed(a=None, version=2) method of random.Random instance
Initialize internal state from a seed.
The only supported seed types are None, int, float,
str, bytes, and bytearray.
None or no argument seeds from current time or from an operating
system specific randomness source if available.
If *a* is an int, all bits are used.
For version 2 (the default), all of the bits are used if *a* is a str,
bytes, or bytearray. For version 1 (provided for reproducing random
sequences from older versions of Python), the algorithm for str and
bytes generates a narrower range of seeds.
Setting the seed doesn’t result in the same number over and over again. But we will get the same sequence of numbers.
2)
seed(0, 359)
randint(0, 359)
randint(2)
seed(0, 359)
randint(0, 359) randint(
28
46
28
46
So where I can get true randomness? Typically we need an external, “natural”, source such as the human user or atmospheric noise. For many applications you can use random.org.
Turtle
In the upcoming programming assignment we are going to use the turtle
module. turtle
mimics the programming language Logo.
from turtle import *
Recall that this imports all the functions, etc. from the turtle
module into our symbol table. In general this is not our preferred approach but I do it here to make class more efficient (less “live” typing).
turtle
works by moving a “pen” around the screen, e.g. to draw a square could the use the forward and right turn functions. Those functions do exactly as their names suggest.
def draw_square():
100)
forward(90)
right(100)
forward(90)
right(100)
forward(90)
right(100)
forward(90) right(
draw_square()
Starting to think about loops
That repetition is very tedious (and not very DRY). We know we want to move and turn 4 times. Can we loop over those statements 4 times? Yes. With a for
loop. A for
loop does exactly what it sounds like. “For” some set of iterations, execute the statements in the body. Note, that we also convert that fixed side length into function parameter that we can change (making our function more flexible and useful).
def draw_square_with_loop(side_length):
for i in range(4):
forward(side_length)90) right(
100) draw_square_with_loop(
Notice we get the same output, but much more concisely!
Comments
In your programming assignments we will ask you to comment your code. The purpose of the comments is to describe what your code is intended to do. Not to describe the code itself, assume the reader knows Python. But they don’t necessarily know what you are trying to do.
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 the#
on the line will not be executed by Python. The second is a structured mechanism for documenting a 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):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.What is the difference between docstrings and inline comments? The intended audience:
The docstring informs the output of the help functionality. You can obtain the documentation for functions, etc. with
help
. For example:or
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.
A common question is that aren’t our docstrings repetitive, i.e., it seems that the description and return value are very similar. Yes. Especially for the simple functions we have been writing so far. But as our functions get more complicated, the description and return value sections will be more complementary.