A115 Software Engineering

Bespoke cloud-based software platforms powering UK commerce since 2010

Re-introduction to Python - part 5. Conciseness.

In this lesson we won't be learning any fundamentally new concepts. Instead, we will focus on some rather fun and powerful short-hand notation for doing things we mostly already know how to do.

First things first. Remember how we said early on that, in some cases, you can ask Python to transform one data type to another? For example, if you have an integer, like the number 5, you can turn it into a string by calling str(5), which returns the string "5" (which is different from the number 5 because it is a different data type.) Or you can turn an integer into a floating-point decimal number by calling float(5), which gives you the value 5.0 - again, mathematically equivalent to 5, but a different data type. This transformation of a value from one type to another is called "type casting" (or just "casting"). You can also cast a float to an integer, but then you're likely to lose some information, because you only get the integer part of the number: int(3.95) returns the integer 3.

Type casting doesn't only work with basic data types, it works for collections as well. You can cast a set into a list, like this: list({1,2,3}) returns [1, 2, 3]. Or you can do the opposite - which, by the way, is a fantastically handy way to get only the unique items in a list: set([1, 2, 3, 2, 3, 4, 3, 5]) returns the set {1, 2, 3, 4, 5}.

When working with loops, it is often useful to iterate a certain number of times - or over a specific range of numbers. Python has a built-in range() function. You can think of it as a "number generator". It generates sequences of numbers. If you only give it a single argument (it has to be a positive number), it generates all the integers from 0 up to (but not including) that number. For example, range(5) generates the numbers from 0 to 4, in order. It intentionally starts at 0, because as we mentioned earlier, in computer programming the items in a collection are typically indexed beginning with 0, not 1. However, range() also supports a different syntax. If you call it with two arguments, it starts counting at the first one and goes up to (but excluding) the second one. So range(10, 13) generates the numbers 10, 11, and 12. Finally, if you call it with 3 arguments, the third one is used as a "step" to count by. So range(2, 20, 2) generates all the even numbers from 2 to 18.

If you try to actually play with range() in the Python interpreter, you'll soon discover something that may annoy you. The return value of range() is not, as you might expect, a list. Instead, it is something called a "range object", which at first glance kind of looks like it's just parroting back what you asked for. There is a good reason for this. We won't get into it now, but what matters is that if you do want to get a list of numbers out of the range() function, all you need to do is cast that range object into a list. In other words, you can just do list(range(5)) to get the list [0, 1, 2, 3, 4].

The cool thing about the range object, though, is that if you just want to iterate over those numbers (like, in a loop), you don't actually need a list. The range object itself is "iterable", so you can do pirate things like:

for n in range(99, 0, -1):
    print(n, "bottles of rum on the wall")

Ok, now we can start learning some tricks.

How would you create a Python list containing the squares (x * x) of all positive integers up to 5000? You might be inclined to use a loop, but as I promised, there is a shortcut:

squares = [x * x for x in range(1, 5001)]

This is called a "list comprehension" and it is very commonly used in Python (while rarely seen in most other languages). It's like a mini "for" loop on a single line. It's a way of generating lists that fit a certain pattern.

For even more power, list comprehensions also have support for filtering - choosing which items make it into the new list. So you can start with some original collection (or something that generates one, like range()) and you can filter it and transform it into a new list, all with a single line of code.

Let's say we have a list of countries and we want a new list which contains only the ones that start with a vowel - and we also want those in UPPERCASE. There is a list comprehension for that:

vowel_countries = [c.upper() for c in countries if c[0] in 'AEIOU']

Are you following along?

In the same way (and almost the same syntax) list comprehensions are used to generate lists, set comprehensions are used to generate sets. Here is a way to get the set of letters with which country names begin:

first_letters = {c[0] for c in countries}

The only difference is the curly brackets (which, as you'll recall, are used to define a set in Python).

Finally, there is also a similar shorthand notation for creating dictionaries. Naturally, it's called a "dictionary comprehension" and also uses curly brackets - see if you can notice the difference from a set comprehension:

squares = {n: n*n for n in range(50)}

This creates a dictionary that maps the first 50 integers to their squares.

Here are some further topics for research and discussion:

  1. What is a built-in Python function to add up the values of all the numbers in a list? What's a built-in Python function to find the smallest number in a list? How about the largest?
  2. What do the functions any() and all() do in Python?
  3. Write a function which takes a list of numbers and returns the sum of their squares. Does your function still work if the input list is empty? Why (or why not?)
  4. Write a function called apply() which takes two arguments:
    • A list of items;
    • Another function f, which takes a single value and returns a single value;
    • Your function should return a list in which each value is the result of applying the function f on the corresponding value in items.
  5. What is a "higher-order function"? What can you learn about "functional programming"?