Zero to Hundred in Python

Python is a general-purpose programming language, excellent if you want to begin learning how to program or build a company to take over the world.

Programming is a powerful skill and Python is the easy entry point.

Without wasting much time, this post provides a zero to hundred with Python… with some tips at the end.

Here is the source code.

Here is a video demonstration below:

Printing and Variables

Lesson number 1 (or 0…), printing to the console. We open up the Python interpreter:

$ python

# or

$ pipenv run ipython

Next, we write our notorious first line of code.

print("Hello World")
>>> Hello World

Next, we learn about commenting code.

# this is a comment and will not be
# evaluated like code

Variables store values, or they point to places in memory in which these values are stored.

number = 10
string = "This is a string"

print(number)
>>> 10
print(string)
>>> "This is a string"

We can use f-strings to print our code within a string, using curly braces to enclose our variable names or Python code.

print(f"number: {number}, and string: {string}")
>>> "number: 10, and string: This is a string"

Variables aren’t all the same. They have types. For example, number is an integer and string is a collection of characters, called a string.

print(f"{type(number} and {type(string)}")
>>> "<class 'int'> and <class 'str'>"

Functions

If we have code we have to write, again and again, it’s good practice to not repeat ourselves. Instead of repeating ourselves, we put this code in a function and call this function. Let’s create a function for the Fibonacci sequence, where we pass a number, n and this will give the nth sequence.

# the fibonacci sequence
# 0, 1, 1, 2, 3, 5, 8...

def f(n: int) -> int:
    """
    Fibonacci sequence
    ---
    params
    n (int) number requested in sequence

    returns
    integer in fibonacci sequence
    """
    if n == 0:
            return 0
    elif n == 1 or n == 2:
            return 1
    else:
            return f(n-1) + f(n-2)

There is a number of notions going on in this above code. Firstly, we introduce function using def. We use typing, where we accept n as an integer (n: int) and the function will return an integer (-> n).

We include a doc-string enclosed in triple quotes ("""). The primary purpose of the doc-string is to explain what the function does.

Also, this function is a good example of conditionals (also known as flow control, because we are controlling the flow of the execution). The if statements checks if the following is True or False. If True, then the code block is executed.

NOTE: Indentation in Python is important.

If the if statement is False, then elif (short for else-if) is checked. We can see the use of or meaning only n == 1 or n == 2 need to be true for the code block to fire, in this case returning 1. Finally, else is fired when the previous if and elif statements are False.

This line, f(n-1) + f(n-2) is an example of recursion. The function f calls itself but with one less n and two less n.

Lists

We can do a lot with functions and variables. Let’s have a look at different data types in Python starting with lists.

We set up a range.

r = range(10)
r
>>> range(0, 10) # number from 0 up to and not including 10

We create a list.

lst = list(r)
lst
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

If we want to access elements in the list, we do this:

lst[0] # the first element of the list is "0th" index
>>> 0
lst[1:3] # the second and up to and not including the 3rd element
>>> [1, 2]
lst[-1] # the last element
>>> 9
lst[::-1] # reverses the list
>>> [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Lists don’t need to be integers, they can be functions.

def f1(x: int) -> str:
    return f"f1: {x + 1}"

def f2(x: int) -> str:
    return f"f2: {x ** 2}"

def f3(x: int) -> str:
    return f"f3: {1 / x}"

funcs = [f1, f2, f3]

for func in funcs:
    print(func(2))

>>> "f1: 3"
>>> "f2: 4"
>>> "f3: 0.5"

This is a good way to introduce the notorious for loop. A list is an example of an iterator. We can use a for loop to iterate for a list for example.

for i in lst:
    output = f(i)
    print(output, end=", ")

>>> "0, 1, 1, 2, 3, 5, 8, 13, 21, 34"

Instead of printing a list of numbers, we can return a list.

output = []
for i in lst:
    output.append(f(i))

output
>>> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Instead of creating an empty list and utilising multiple lines, we can turn this into a ‘one-liner’. We use the infamous list comprehension.

output = [f(i) for i in lst]
output
>>>[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Importing Libraries

A lot of the time, we will be using other people’s well-written code. This saves time, but also there are some very intelligent people on this planet. They are providing solutions to problems that I could not solve in my lifetime for FREE. One example is a library called pandas, which is popular in data analytics.

Back to our Fibonacci function, you will notice the problem with recursions is that the more numbers we request in the sequence, the longer the program takes.

We can show this using IPython’s magic function, %timeit.

times = []
for i in range(33): # we will do the first 32 numbers in sequence
    t = %timeit -n1 -o f(i)
    times.append(t.best)

The -n<N> tag means how many times a function will loop, in our case, only once; -o outputs the timeit result to a variable, t in our case, for later inspection.

Now we will use pandas to tabulate our data.

import pandas as pd

df = pd.DataFrame(data=times, columns=["f"]
df.head() # prints first five results by default
>>>
f
0  5.829997e-07
1  6.790001e-07
2  7.760000e-07
3  1.695000e-06
4  2.799000e-06

We have given pandas the alias pd. This saves us having to write “pandas” all the time.

By importing pandas we have access to the DataFrame class. We can plot this data and save it. We use another library, matplotlib, which is a graphing library to save our graph as an image.

import matplotlib.pyplot as plt

g = df.plot(title="Time on Fib")
g.set_xlabel("n")
g.set_ylabel("times (s)")
plt.savefig("f.png")

Very expensive function

We can see the exponential rise in the time the program takes as n rises.

We can calculate the total time:

df.sum()
>>> f    3.693825 # total time
>>> dtype: float64

More data types — Dictionaries

We will introduce dictionaries. Dictionaries have a key and a value pair.

dct = {
    "key1": "value1",
    "key2": ["value2", "value3"],
    "random name for a key": 1,
    "key4": (2000, 3000)
}

And to get a value from a dictionary:

dct["key4"]
>>> (2000, 3000) # this is a tuple by the way

If we refer to a key that doesn’t exist (e.g. dct["key5"], we get an error. Alternatively we can use the get() method, so our code doesn’t error and get None returned instead.

dct.get("key5") is None
>>> True

We can even apply a for loop to dictionaries.

for k, v in dct.items():
    print(f"key: {k}, value: {v}")

>>> key: key1, value: value1
>>> key: key2, value: ['value2', 'value3']
>>> key: key3, value: 1
>>> key: key4, value: (2000, 3000)

Coming back to our Fibonacci function example, we can use dictionaries to store some previously calculated values instead of calculating then again and again. This is called caching and what we are doing is trading memory for less compute time.

cache = {}

def f_better(n: int) -> int:
    """
    Fibonacci sequence - !!with caching!!
    ---
    params
        n (int) number requested in sequence

    returns
        (int) in fibonacci sequence
    """
    if all([cache.get(n-1), cache.get(n-2)]):
        return cache[n-1] + cache[n-2]
    elif n == 0:
        return 0
    elif n == 1 or n == 2:
        return 1
    else:
        cache[n] = f_better(n-1) + f_better(n-2)
        return cache[n]

We have added a conditional in the beginning that simple checks if the previous result is in the cache and uses this stored value instead of calculating from scratch all the time.

Another addition is that the newly calculated value is stored in the cache.

We can measure how long this new function runs.

times = []
for i in range(35):
    t = %timeit -n1 -o -q f_better(i)
    times.append(t.best)

df['f_better'] = pd.DataFrame(data=times)
df.tail(10)
>>>
f             f_better
23  0.005453  5.040001e-07
24  0.008786  1.269000e-05
25  0.014167  1.357200e-05
26  0.022997  4.909998e-07
27  0.037203  1.430600e-05
28  0.060064  1.520500e-05
29  0.097307  5.379998e-07
30  0.157304  1.653000e-05
31  0.254740  1.687500e-05
32  0.412715  5.299999e-07

We can plot this.

g = df.plot(title="Comparing")
g.set_xlabel("n")
g.set_ylabel("time (s)")
plt.savefig("f_better.png")

We have done better

We can see that the time taken is dramatically less with this caching implementation.

Finally, we can print this cache.

cache
>>> {
        3: 2,
        4: 3,
        6: 8,
        7: 13,
        9: 34,
        10: 55,
        12: 144,
        13: 233,
        15: 610,
        16: 987,
        18: 2584,
        19: 4181,
        21: 10946,
        22: 17711,
        24: 46368,
        25: 75025,
        27: 196418,
        28: 317811,
        30: 832040,
        31: 1346269,
        33: 3524578,
        34: 5702887
}

Classes

In Python, everything are objects. To create our own objects, we create classes.

class Car:
    """
    Class for cars that people drive
    ---
    params
        make (str): make of the car
        model (str): model of the car
        year (int): year car was made
    """
    def __init__(self, make: str, model: str, year: int=2021):
        self.make = make
        self.model = model
        self.year = year
        self.__is_driving = False

    def __repr__(self):
        return f'{self.make} - {self.model}'

    def drive(self) -> str:
        if self.__is_driving == False:
            print(f'{self.make} - {self.model} is now driving')
            self.__is_driving = True
        else:
            print(f'{self.make} - {self.model} is already driving!')

    def stop(self) -> str:
        if self.__is_driving == True:
            print(f'{self.make} - {self.model} has stopped')
            self.__is_driving = False
        else:
            print(f'{self.make} - {self.model} is already stopped!')

Let’s test drive our new class.

shivan = Car('Toyota', 'Corolla', '2008')
bruno = Car('Tesla', 'Model3')

We have two objects, shivan and bruno derived from the Car class.

bruno
>>> "Tesla - Model3"

shivan.drive()
>>> "Toyota - Corolla is now driving"

shivan.drive()
>>> "Toyota - Corolla is ALREADY driving"

shivan.stop()
>>> "Toyota - Corolla has stopped"

bruno.stop()
>>> "Tesla - Model3 is ALREADY stopped!

Ending notes

So now you have seen the basics of Python. This is only the beginning. Go building something. Go be somebody!

Whatever interests you big or small, go make it. Don’t be afraid of making mistakes, but do ask for help when needed. Build a habit, small steps every day, leads to a large distance over time.

Finally, if you have any feedback, suggestions, or questions please let me know. Remember to respect yourself, stay Pythonic.

Ka kite anō!