Dark theme

Key Ideas


The key ideas for this part centre on basic exception handling and functional programming.


Exceptions

The core exception handling structure is shown by this example:

import random
try:
    a = 1/random.random()
except:
    a = 0
finally: # Stuff that definitely must happen.
    if a == None: a = -1
print("Done")

If you know what exceptions are likely (list), you can catch each and respond to them differently. You can also catch generic Exception:

import random
try:
    a = 1/random.random()
except ZeroDivisionError:
    a = 0
except Exception:
    a = -1
finally: # Stuff that definitely must happen.
    if a == None: a = -1
print("Done")

If for some reason you need to end the program, you can do so with this call:
sys.exit()
which takes an optional int error code as an argument and returns it to the operating system.


Functional programming

In Python, a great deal of the functional elements fall around functions that use or apply other functions, which they take in as arguments. Here are some of the major examples.

The sorted function can take in a function as a "key" on which to search. The key function is called and passed each item in the iterable container to be sorted. Note that the key should be given as function_name not usually function_name() (which actually calls the method there and then to return something).

new_list = sorted(iterable, key=function_name, reverse=False)

The map(function, iterable, ...) function takes in a function and iterable container (list, tuple, etc.), and applies the function to each element. For example:

for value in map(ord, ["A","B","C"]): # ord returns ACSII numbers
    print(value)

One option with map is to pass in a short 'headless' (nameless) or 'anonymous' function. Anonymous functions are denoted with the keyword lambda. Amongst other things, this allows multiple containers to be processed.

for square in map(lambda x: x**2, [1,2,3,4,5]):
    print(str(square)) # 1, 4, 9, 16, 25

for added in map(lambda x,y: x+y, [1,2,3,4,5],[1,2,3,4,5]):
    print(str(added)) # 2, 4, 6, 8, 10

b = "fire walk with me".split(" ")
for a in map(lambda x: x[::-1], b):
    print(a) # erif klaw htiw em

The itertools library includes a variety of methods that work in similar ways, including some that aggregate the results of operations across the iterable in some way.

We can set up lists in a similar way. In 'list comprehensions' the function is written directly into the list setup. These tend to be relatively simple expressions.

listA = [1,2,3,4]
listB = [2*x for x in listA]
print(listB) # [2, 4, 6, 8]

listB = [2* x for x in range(1:5)]
print(listB) # [2, 4, 6, 8]

listB = [2*x for x in listA if x % 2 == 0] # Even numbers only
print(listB) # [4, 8]

Can also be used to apply a method to everything in a list.
listA = ['A','B','C']
listB = [ord(x) for x in listA]
print(listB) # [65, 66, 67]

They can also be used like a nested loop structure:

listA = [(x, y) for x in [1,2,3] for y in ["a","b","c"]]
print(listA)

[(1, 'a'), (1, 'b'), (1, 'c'), (2, 'a'), (2, 'b'), (2, 'c'), (3, 'a'), (3, 'b'), (3, 'c')]

As list comprehensions generate all the data at first execution and then copy it in, for very large lists, use generator expressions (the same, but with parentheses rather than square brackets) which generates items one at a time, as needed, and are therefore more memory efficient.

Generator functions are similar in that they generate a value at a time. They return a value at yield and wait for the next call. For example:

def count():
    a = 0
    while True:
        yield a # Returns control and waits next call.
        a = a + 1
b = count()
print (next(b))
print (next(b))

Where the condition is just set to True they will generate an infinite sequences. They must be attached to a variable so the same instance is called each time and the method-level variables aren't wiped; this doesn't work:

print (next(count()))