Lecture 3

Objectives for today

  1. Obtain the documentation for functions and other Python objects
  2. Describe aspects of good coding style
  3. Explain the purpose of modules, and use standard modules like math and turtle

Good Coding Practice Continued

A little about style

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.

Comments

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:

  1. Block and inline comments
  2. Docstrings

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

Being DRY

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!

Constants

Consider the following function for the Lorenz factor by which time changes for objects with relative 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. This is much more readable.

Someone is no doubt 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 constant. Instead it is a convention. You can change the value, but shouldn’t.

Modules

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. But as you might imagine, since those are common operations, someone already has.

Python has whole modules of functionality that can be imported and reused.

import math

SPEED_OF_LIGHT=299792458

def lorenz(velocity):
	return 1 / math.sqrt(1 - (math.pow(velocity,2) / math.pow(SPEED_OF_LIGHT, 2)))

You can import 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

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)

Extra: Why not 360? Recall randint is inclusive and 0 is the same as 360. We would slightly oversample not making any turn at all.

Can computers generate truly random numbers? No. Because the algorithm (and the computer) and 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 algorihtm 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.
    
    For version 2 (the default), all of the bits are used if *a* is a str,
    bytes, or bytearray.  For version 1, the hash() of *a* is used instead.
    
    If *a* is an int, all bits are used.

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? For many applications you can use random.org.

Organization of our files

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:

  1. Docstring describing the file (in our case name, section, assignment, etc.)
  2. Module imports
  3. Constants
  4. Functions

Summary

  1. Constants
  2. Introduction to modules
  3. randomness

Next time: Loops