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 shell, both functions produce the same output
>>> double_it(12)
24
>>> another_double_it(12)
24
but they are not the same (the different typeface is a bit of a hint…). 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 before. That is
Python implicitly adds a return None
to the end of the function.
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 Lab 1 you will 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 (beyond just returning a value).
PI Questions
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 what your code is doing. Not to describe the code itself, assume the reader knows Python. But they don’t know 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, 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
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 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., """
. 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. 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
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 get more complicated, the description and return value sections will be complementary.
Consider the following function for the Lorenz factor by which time changes for an object with relativistic velocity (my apologies to any physicists).
def lorenz(velocity):
return 1 / (1 - (velocity ** 2) / (300000000 ** 2)) ** 0.5
What is 300000000
? Does that change?
SPEED_OF_LIGHT=300000000
def lorenz(velocity):
return 1 / ((1 - (velocity ** 2) / (SPEED_OF_LIGHT ** 2)) ** 0.5
Note, we use all capitals to indicate constants.
Someone is likely thinking my SPEED_OF_LIGHT
is just an approximation.
SPEED_OF_LIGHT=299792458
is much more accurate. If I didn’t use a constant I would need to change every
use of 300000000
in my code.
So in general we use constants to improve readability and to ease making changes. There are many places in Lab 2 where you 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 convention. You can
change the value, but shouldn’t.
In the Lorenz function we computed 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:
SPEED_OF_LIGHT=299792458
def lorenz(velocity):
def sqrt(val):
return val ** 0.5
return 1 / sqrt(1 - (velocity ** 2) / (300000000 ** 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 inside lorenz
, 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. Thus part of good style is not making things more complicate than needed.
We would approach this instead as:
SPEED_OF_LIGHT=299792458
def sqrt(val):
return val ** 0.5
def lorenz(velocity):
return 1 / sqrt(1 - (velocity ** 2) / (300000000 ** 2))
But we can do even better. As you might imagine, since square root and squaring are common operations, someone already has 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
SPEED_OF_LIGHT=299792458
def lorenz(velocity):
return 1 / math.sqrt(1 - (math.pow(velocity, 2) / math.pow(SPEED_OF_LIGHT, 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 a
number of 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 in Lab 2. 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?
randint(0, 359)
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 hashable object.
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.
>>> seed(2)
>>> randint(0, 359)
28
>>> randint(0, 359)
46
>>> seed(2)
>>> randint(0, 359)
28
>>> randint(0, 359)
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.
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: