“Explicit is better than implicit”

What does this line of the poem refer to? Well, there are several examples and interpretations one could give, but today I would to interpret this line with reference to the idea of readability.

We wrapped up part one by talking about a function that contains no defenses against misuse. To jog your memory, here's the code again:


In [9]:
def net_force(mass, acceleration):
    return mass * acceleration

Literally this function takes two objects and returns mass * acceleration; But what does that actually mean? And why should we care?

Well, the meaning of this code is implicit, the author just assumes you are going to understand what it does and how to use it. The premise of today's lecture however is that this code can be rewritten to be more explicit, and being explicit is generally to be prefered.

To be more precise, the code make two implicit assumptions:

  1. The end user has a basic understanding of Physics.
  2. The end user knows to pass it numbers.

Regarding the second point above I'm not going to explain why this is a problem today (for that explanation, see lecture on Operator Overloading), but I will propose a few possible fixes. But first things first, lets talk physics!

To People with a modest background in Physics it is pretty obvious all we are doing is taking the formula for force, f=ma, and putting it in code form.

But to understand the problem with the code, we need to imagine talking to someone that NEVER took a physics lesson at school before. For this person, they understand that we are multiplying two numbers (i.e. Mass, Acceleration) but they have not concept of what the result actually means. In short, the code only returns force if you know the physics, for everyone else the function return simply returns two numbers multiplied together.

In short, the code is being implicit but the poem tells us to be explicit. Alright, let's try and fix that now...


In [1]:
def net_force(mass, acceleration):
    force = mass * acceleration
    return force

print(net_force(10,10))


100

Defining Force

This code does one more thing that makes things more explicit. instead of returning:

Mass * Acceleration

we now return:

Force 

...And the line above the return statement clearly assigns force to 'Mass * Acceleration'. So now even those readers without the physics background understand the meaning behind what we are doing; the function isn't merely returning A times B, its returning force.

So, we have succesfully made our code more explicit, even readers without an understanding of physics can grasp what the number we are returning actually means.

However, there is a problem with this:

"Do we really want to add code to our function whose sole purpose is to make things readable?"

Honestly that is exactly what we have done in this case, defining and then returning force simply adds an unnecessary step. Unnecessary lines of code harms readability which is ironic since we only added this unnecessary line to make the code more readable!

As a matter of fact there is a solution to this problem, we could just add an in-line comment like this:

# ...and now we return force...
return mass * acceleration

Such a comment makes the code more explicit, but doesn't add unnecessary code. So already this looks like a better solution. It turns out though we can do even better than a comment, we can add a ‘docstring’:


In [ ]:
def net_force(mass, acceleration):
    """
    Calculates f=ma, returns force.
    We assume mass & acceleration are of type int/float.
    """
    return mass * acceleration

Docstrings

The above code has a new concept to talk about. The red text encased in triple quotes is called a docstring, it is a special type of string that Python Programmers use to communicate with each other. Usually docstrings contain information about how the function works, and how to use it. Such is the case here. Docstrings can also serve as another way to write comments that span several lines. The Syntax is really simple:

"""
{Text}
"""

The docstring above tells use that the function expected us to pass in integers/floats, and it also mentions what it is supposed to do (i.e. calculate f=ma). In short, adding documentation has helped make our function much more explicit. And for what it is worth, adding docstrings to all your functions is generally regarded as good practice.

Its often a good idea to mention the expected 'types' of input. If you expect somebody to pass in numbers then say so. If you the code is supposed to work with strings then say so; Explicit is better than implicit.

Being explicit about what your code expects can prevent bugs, for example:


In [15]:
def net_force(mass, acceleration):
    """
    Calculates f=ma, returns force.
    We assume mass & acceleration are of type int/float.
    """
    return mass * acceleration

print(net_force("10", 10))


10101010101010101010

This output might surprise you (and I'll explain why this happens in later lectures). Notice that the function net_force says it expects numbers. We passed in a string and an integer. And so, if we are using the function in an unexpected/unintended way should we surprised when things do not work as we expect?

So docstrings/comments can avoid bugs because they increase the chances that somebody will use some piece of code as it was designed to be used. If only I read the documentation, I would have known to not pass in a string!

Okay, so that explains why we should leave text for other developers to read, but why are Docstrings better than comments? Well, the main reason is that docstrings make for better documentation. For example:


In [13]:
help(int)


Help on class int in module builtins:

class int(object)
 |  int([x]) -> integer
 |  int(x, base=10) -> integer
 |  
 |  Convert a number or string to an integer, or return 0 if no arguments
 |  are given.  If x is a number, return x.__int__().  For floating point
 |  numbers, this truncates towards zero.
 |  
 |  If x is not a number or if base is given, then x must be a string,
 |  bytes, or bytearray instance representing an integer literal in the
 |  given base.  The literal can be preceded by '+' or '-' and be surrounded
 |  by whitespace.  The base defaults to 10.  Valid bases are 0 and 2-36.
 |  Base 0 means to interpret the base from the string as an integer literal.
 |  >>> int('0b100', base=0)
 |  4
 |  
 |  Methods defined here:
 |  
 |  __abs__(self, /)
 |      abs(self)
 |  
 |  __add__(self, value, /)
 |      Return self+value.
 |  
 |  __and__(self, value, /)
 |      Return self&value.
 |  
 |  __bool__(self, /)
 |      self != 0
 |  
 |  __ceil__(...)
 |      Ceiling of an Integral returns itself.
 |  
 |  __divmod__(self, value, /)
 |      Return divmod(self, value).
 |  
 |  __eq__(self, value, /)
 |      Return self==value.
 |  
 |  __float__(self, /)
 |      float(self)
 |  
 |  __floor__(...)
 |      Flooring an Integral returns itself.
 |  
 |  __floordiv__(self, value, /)
 |      Return self//value.
 |  
 |  __format__(self, format_spec, /)
 |      Default object formatter.
 |  
 |  __ge__(self, value, /)
 |      Return self>=value.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __getnewargs__(self, /)
 |  
 |  __gt__(self, value, /)
 |      Return self>value.
 |  
 |  __hash__(self, /)
 |      Return hash(self).
 |  
 |  __index__(self, /)
 |      Return self converted to an integer, if self is suitable for use as an index into a list.
 |  
 |  __int__(self, /)
 |      int(self)
 |  
 |  __invert__(self, /)
 |      ~self
 |  
 |  __le__(self, value, /)
 |      Return self<=value.
 |  
 |  __lshift__(self, value, /)
 |      Return self<<value.
 |  
 |  __lt__(self, value, /)
 |      Return self<value.
 |  
 |  __mod__(self, value, /)
 |      Return self%value.
 |  
 |  __mul__(self, value, /)
 |      Return self*value.
 |  
 |  __ne__(self, value, /)
 |      Return self!=value.
 |  
 |  __neg__(self, /)
 |      -self
 |  
 |  __or__(self, value, /)
 |      Return self|value.
 |  
 |  __pos__(self, /)
 |      +self
 |  
 |  __pow__(self, value, mod=None, /)
 |      Return pow(self, value, mod).
 |  
 |  __radd__(self, value, /)
 |      Return value+self.
 |  
 |  __rand__(self, value, /)
 |      Return value&self.
 |  
 |  __rdivmod__(self, value, /)
 |      Return divmod(value, self).
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  __rfloordiv__(self, value, /)
 |      Return value//self.
 |  
 |  __rlshift__(self, value, /)
 |      Return value<<self.
 |  
 |  __rmod__(self, value, /)
 |      Return value%self.
 |  
 |  __rmul__(self, value, /)
 |      Return value*self.
 |  
 |  __ror__(self, value, /)
 |      Return value|self.
 |  
 |  __round__(...)
 |      Rounding an Integral returns itself.
 |      Rounding with an ndigits argument also returns an integer.
 |  
 |  __rpow__(self, value, mod=None, /)
 |      Return pow(value, self, mod).
 |  
 |  __rrshift__(self, value, /)
 |      Return value>>self.
 |  
 |  __rshift__(self, value, /)
 |      Return self>>value.
 |  
 |  __rsub__(self, value, /)
 |      Return value-self.
 |  
 |  __rtruediv__(self, value, /)
 |      Return value/self.
 |  
 |  __rxor__(self, value, /)
 |      Return value^self.
 |  
 |  __sizeof__(self, /)
 |      Returns size in memory, in bytes.
 |  
 |  __str__(self, /)
 |      Return str(self).
 |  
 |  __sub__(self, value, /)
 |      Return self-value.
 |  
 |  __truediv__(self, value, /)
 |      Return self/value.
 |  
 |  __trunc__(...)
 |      Truncating an Integral returns itself.
 |  
 |  __xor__(self, value, /)
 |      Return self^value.
 |  
 |  bit_length(self, /)
 |      Number of bits necessary to represent self in binary.
 |      
 |      >>> bin(37)
 |      '0b100101'
 |      >>> (37).bit_length()
 |      6
 |  
 |  conjugate(...)
 |      Returns self, the complex conjugate of any int.
 |  
 |  to_bytes(self, /, length, byteorder, *, signed=False)
 |      Return an array of bytes representing an integer.
 |      
 |      length
 |        Length of bytes object to use.  An OverflowError is raised if the
 |        integer is not representable with the given number of bytes.
 |      byteorder
 |        The byte order used to represent the integer.  If byteorder is 'big',
 |        the most significant byte is at the beginning of the byte array.  If
 |        byteorder is 'little', the most significant byte is at the end of the
 |        byte array.  To request the native byte order of the host system, use
 |        `sys.byteorder' as the byte order value.
 |      signed
 |        Determines whether two's complement is used to represent the integer.
 |        If signed is False and a negative integer is given, an OverflowError
 |        is raised.
 |  
 |  ----------------------------------------------------------------------
 |  Class methods defined here:
 |  
 |  from_bytes(bytes, byteorder, *, signed=False) from builtins.type
 |      Return the integer represented by the given array of bytes.
 |      
 |      bytes
 |        Holds the array of bytes to convert.  The argument must either
 |        support the buffer protocol or be an iterable object producing bytes.
 |        Bytes and bytearray are examples of built-in objects that support the
 |        buffer protocol.
 |      byteorder
 |        The byte order used to represent the integer.  If byteorder is 'big',
 |        the most significant byte is at the beginning of the byte array.  If
 |        byteorder is 'little', the most significant byte is at the end of the
 |        byte array.  To request the native byte order of the host system, use
 |        `sys.byteorder' as the byte order value.
 |      signed
 |        Indicates whether two's complement is used to represent the integer.
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  denominator
 |      the denominator of a rational number in lowest terms
 |  
 |  imag
 |      the imaginary part of a complex number
 |  
 |  numerator
 |      the numerator of a rational number in lowest terms
 |  
 |  real
 |      the real part of a complex number

we can use the "help" command to get information about a python objects. As a implementation detail, "help" will return the docstring of the function. Thus, if we use docstrings then other developers can call 'help' on our functions to find out what they do. Cool right?


In [10]:
def add_eight_1(x):
    """
    Takes a integer x, and returns x + 8
    """
    return x + 8

help(add_eight_1)


Help on function add_eight_1 in module __main__:

add_eight_1(x)
    Takes a integer x, and returns x + 8


In [11]:
def add_eight_2(x):
    # Takes a integer x, and returns x + 8
    return x + 8

help(add_eight_2)


Help on function add_eight_2 in module __main__:

add_eight_2(x)


In [ ]: