title

Python as a Programming Language

So far we've seen Python do some very simple things, that software like Excel could do just as well. What sets Python apart from other programming languages and software is it's ability to easily make automated programs quickly. Here we will look at the building blocks of writing programs, and think about how we can use them in a real environment.

A Deeper Look Into Functions

Functions are an integral part of Python - throughout your time with Python you will use many functions written by other people, and hopefully some you write yourself. A function can take in arguements as inputs, then performs a process, and then returns an output. Let's take a look at the pow() function to see what's going on:

In [1]:
print(pow(3,4))
81

The pow() function takes in two arguements, and returns the first arguement to the power of the second. Let's take a look at how to write this function ourselves:

In [2]:
def myPow(a,b):
    return a ** b

The most important thing to remember here is the whitespace between the left margin and the second line; Python cares about indentations. In function definitions, we declare the name and arguements of a function, and then write the process, and what to return in an indented block. The names of the arguements don't matter - they are just dummy variables, used to give Python an example of what to do.

You might have noticed instead of printing you can just output the variable straight away:

In [1]:
a = 3
a
Out[1]:
3

What does this have to do with functions? The number one problem with function definitions is not knowing the difference between return and print. A function can only "return" one thing - when the function returns something, it ends, because the output has been reached. A function can "print" as many things as it wants - but won't treat these as outputs. This can lead to some interesting errors - let's look at what happens when we chain functions together written with print:

In [4]:
def myAdd(a,b):
    print(a + b)
In [5]:
myAdd(3,5) #Nothing looks wrong here, right?
8
In [6]:
myAdd(myAdd(3,5), 5) #Oops
8
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-4d6839590591> in <module>
----> 1 myAdd(myAdd(3,5), 5) #Oops

<ipython-input-4-7ff1bc9f87ba> in myAdd(a, b)
      1 def myAdd(a,b):
----> 2     print(a + b)

TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

With return, we don't have this problem:

In [ ]:
def myAdd(a,b):
    return a + b
In [ ]:
myAdd(myAdd(3,5),5)

"Outputs" are helpful results of functions that actually mean something - "Prints" are just visuals to help you out!

In short, if your function doesn't return anything, there's probably a problem!

If Statements

Sometimes when writing a program we want Python to make choices for us depending on the input. If statements are usually used in a context with variables that change, for example functions. Let's look at the syntax with a simple function that tests if the input is 10 or not:

In [ ]:
def isTen(number):
    
    if number == 10:
        
        return "Yes!"
    
    else:
        
        return "No!"
    
print(isTen(10))
print(isTen(5))

Some important things here - first off the indents again; if statements, like function definitions, need whitespace for Python to understand the code. Secondly, notice that when we construct the condition (number == 10), it's a double equals sign instead of a single. This is because the single equals is already used for setting variables. There are many conditionals we can use (for example: <, >, <=, >= are less than, greater than, less than or equal to, greater than or equal to) - make sure to check out the Python documentation here to see examples of more.

Not all questions are Yes or No - and Python has a way for us to deal with this - elif. For example, say we want to write a program that tells us if a number greater than 5 or less than or equal to 3:

In [ ]:
def fiveOrThree(number):
    
    if number > 5:
        
        return "This is a big number!"

    elif number <= 3:
        
        return "This is a small number!"
    
    else:
        
        return "I don't know how big this number is!"
    
print(fiveOrThree(2))
print(fiveOrThree(5))
print(fiveOrThree(6))

Another helpful conditional to use is in, usually used with lists:

In [3]:
fruits = ["banana", "orange", "strawberry"]
vegetables = ["carrot", "potato", "broccoli"] # Note: not extensive lists of fruit or vegetables

def fruitOrVeg(food):
    
    if food in fruits:
        
        return "fruit"
    
    if food in vegetables:
        
        return "vegetables"
    
    else:
        
        return "unknown"
    
print(fruitOrVeg("banana"))
print(fruitOrVeg("carrot"))
print(fruitOrVeg("soup"))
fruit
vegetables
unknown

Loops

There are two types of loop we'll look at here - the for loop and the while loop. Loops are used to do something simple over and over again to stop us from doing it. For example, say (for some reason) we don't know if the numbers 0 through 10 are bigger than 5 or smaller than 3. Using our function from earlier we can do the following:

In [ ]:
for number in range(11): #range(11) gives us a list of the numbers 0-10
    
    print(fiveOrThree(number))

The for loop executes the indented code for every element in the list range(11).

While loops are slightly diffent - they check for a condition before executing the indented code. For example, a way of rewriting the code above would be:

In [ ]:
number = 0
while number <= 10:
    print(fiveOrThree(number))
    number += 1 #Shorthand way of saying number = number + 1

Finally with loops, sometimes we want to stop the loop prematurely to save time, or when we get what we want. Firstly, let's look at an example that finds the numbers from 2 to 20 that are not prime. A prime number is a number that's only divisible by itself or 1 - so any non-prime number will be divisible by a number from 2 to n. We just need to find what these are:

In [8]:
for number in range(2, 20):
    
    for x in range(2, number): #iterates from 2 to our number
        
        if number % x == 0: #this means our number is divisible f
            
            print(str(number) + " is not prime")
            break


    
4 is not prime
6 is not prime
8 is not prime
9 is not prime
10 is not prime
12 is not prime
14 is not prime
15 is not prime
16 is not prime
18 is not prime

Can we do better than this? What if we want to find what the number's prime factors are? Here's some code that puts all we've learnt together:

In [ ]:
number = 12847
primes = []
test = 2

while True: #Sets up a loops that never ends unless we break it:
    
    if number == 1: #This will be clear later
        
        print("Decomposition found!")
        print(primes)
        break #Finishes program
    
    elif number % test == 0: #If the number is divisible by the test number
            
        primes.append(test) #Adds the prime factor to the primes list
        number /= test #Shorthand for number = number / test
        
    else: 
        test += 1

Let's look at this in more detail - we set up our number, an empty list for the primes, and the test number 2. We then set up an infinite while loop that will only end if we break it, then ask if the number has been fully decomposed into its factors. If this is the case, we print the decomposition and break out of the loop, ending the program. If this isn't the case, we check to see if our test number divides our number. If this happens, we add our test number to the list, and divide our number by the test number. If neither of these conditions are met, we just add 1 to the test number and try again. Eventually, the number will keep being divided by prime numbers and reach 1, at which point the program ends.

At first glance this program may seem complicated, don't worry! Break it down into parts and see if you can understand each bit, then put it all together - once you've done that, try turning this into a function so we can input what "number" is.

Similarly, if this program seems a bit wasteful - you're right! We could turn this into a function to generalise it more, or use better syntax in places to speed the program up. Programming is more of art than a science - there are many "right answers"!

Another Look At Methods

We've already seen a method in the last guide, namely the append method. Let's talk about them a bit more to get comfortable with them, as we'll see them pop up everywhere.

Strings in Python can act as lists - we can iterate over them using for loops, as well as perform some unique methods on them. For example, we can make a string into all upper case using the .upper() method:

In [ ]:
string = "hello python!"
print(string.upper())

Similarly, there are some methods for lists:

In [ ]:
myList = ["Tom","Andy","Pete","Josh"]

print(myList.index("Andy")) #Gives us the index of Andy
print(myList.pop()) #Gives us the last element and removes it
print(myList) #Gives us the list back, with the last element popped out.

Dictionaries

Dictionaries are our first real example of a data structure. They let us assign values to custom indexes:

In [ ]:
HiPy = {"location": "Liverpool University", "startDate": 2016, "language": "Python"}

To get our entries back out of the dictionary we index as if it were a list with custom indexes:

In [ ]:
HiPy["location"]

Dictionaries can help us manage data more intuitively at the price of using more memory. For example, if we wanted to store the marks of three students, we could use a list of lists, which is hard to read:

In [ ]:
marks = [[90,86,70],[60,70,65],[90,40,60]]

Or use a list of dictionaries:

In [ ]:
Adam = {"maths": 90, "english": 86, "science":70}
Billy = {"maths": 60, "english": 70, "science":65}
Chris = {"maths": 90, "english": 40, "science":60}

students = [Adam, Billy, Chris]

Now we can reference everything from the students list, or reference things directly from the students in a more intuitive way:

In [ ]:
print(Adam["maths"])
print(Billy["science"])
print(students[1]["english"])

Worked Example

Let's take a look at a program that outputs the first n numbers of the fibonacci sequence:

In [ ]:
def fib(n):
    a = 1
    b = 1
    fibList = [1,1]
    
    while len(fibList) < n:
        b = a + b
        a = b - a #Shuffles the numbers around so we get the next pair
        fibList.append(b)
    
    return fibList

print(fib(10))

As with our prime example before, we set up our variables, then a while loop that checks to see if we've reached the nth fibonacci number yet. If it hasn't, we generate the next in the sequence and add it to the list. If it has, we return the list and finish the program.

There are many ways of improving this program, including using indexing, or simultaneously declaring what a and b are in the while loop - however this code works well enough for now.

Mini Project

For a long time, the number one programming challenge for programmers hoping to work at big tech companies was the "FizzBuzz" challenge. It involves making a program that given a number, returns Fizz if divisible by 3, and Buzz if divisible by 5, and FizzBuzz if divisible by both. Can you:

  • Make a function that does this?
  • Set up a for loop that runs this function on the numbers 1-100?
  • Using the map funciton, do the same as above, but without using a for loop?