Comprehensions

By: Bryce Tham (Edits by Raelene Gonzales and Minjae Wee)

Comprehensions may seem difficult at first, but I'm here to simplify them!
Traditional creation of lists, tuples, sets, and dicts are perfectly fine for the programs that you are writing now.

But as your projects increase in complexity, comprehensions can save a lot of time and brain power.

Why use comprehensions?

* Trivial multiple-line code ---> ONE LINE.
* Less clutter means better code.

------------------------------------------------------------------------------

List Comprehensions

Let us first begin by discussing the simplest form of comprehensions: list comprehensions.

COMMON USES:
1. Create new lists from existing ones (our focus).
2. Create new lists from iterables such as generators, strings, ranges, etc.

TYPICAL FORMAT:
new_list = [f(i) for i in old_list]

This essentially accomplishes the same thing as the code below:

new_list = []
for i in old_list:
    new_list.append(f(i))

PYTHON'S PROCESS:
1. Goes through each item (i) in old_list.
2. Runs operation (f) on each item (i).
3. Appends f(i) to new_list

Keep in mind... f can be any function as long as it returns a value of any type.
EX #1:

old_list = [1, 2, 3, 4, 5]
new_list = [i+1 for i in old_list]

The resulting new_list would be [2, 3, 4, 5, 6], because for each item i in list, Python runs the operation i+1 and appends i+1 to newlist.

So, we've got list comprehensions down already! Can you guess what comprehensions below print out?
(hint: the same logic from EX #1 applies to comprehensions with iterables. Copy and paste into the console for the solution)

print([i for i in range(10)])
print(['b'+c for c in 'aeiou'])

------------------------------------------------------------------------------

Adding Conditional Statements to Comprehensions

Optionally, we can add a conditional statement (i.e. a Bool) to the end of a comprehension if we want the comprehension to only include certain values.

If old_list = [1, 2, 3, 4, 5], how would we create a new_list that consists of only even numbers?

EX #2 (compare for-loop and comprehension):

NORMAL FOR LOOP:
new_list = []
for i in old_list:
    if i%2 == 0: # checks if i is even
        new_list.append[i]

ALTERNATIVELY:
new_list = [i for i in old_list if i%2 == 0]

Notice the "if statement" at the end of this comprehension.
Here, Python goes through every item in the old_list, and if i%d == 1 is true, i is appended to new_list.
We use this to write even more complex comprehensions like the ones below:

print([i**2 for i in new_list if i%2 == 1])
print([i for i in 'abcde' if i in 'aeiou'])

Can you guess what the comprehensions above produce?

------------------------------------------------------------------------------

Nested Comprehensions (Advanced)

Once you have mastered all the concepts mentioned above, it is time to take comprehensions one step further.

A two-dimensional (2D) list is a list of lists. We can use comprehensions to simplify the following block of code from this...

EX #3:

NORMAL FOR LOOP:
old_list = [[1, 2, 3], [6, 5, 4], [7, 8, 9, 0]]
new_list = []
for sub_list in old_list:
    for i in sub_list:
        new_list.append(i)

ALTERNATIVELY:
old_list = [[1, 2, 3], [6, 5, 4], [7, 8, 9, 0]]
new_list = [i for sub_list in old_list for i in sub_list]

PYTHON'S PROCESS:
1. Iterate through each sub_list in old_list.
2. Iterate through each item (i) in each sub_list.
3. Appends each item (i) to new_list.


EX #4 (Boolean Condition):

list_of_names = [['Bob', 'Jeffrey', 'Ruth'], ['Brian', 'Mike', 'Jessica'], ['Andrew', 'Mary']]
starts_with_a = [i for group in list_of_names for i in group if i.startswith('B')]

At this point, comprehensions may begin to look quite messy, so you may opt to break it into smaller pieces for better readability. The last line of the code above can be rewritten like this:

starts_with_a = []
for group in list_of_names:
    starts_with_a.extend([i for i in group if i.startswith('A')])

It is up to you to determine whether you want to utilize more complex forms of comprehension or break them into pieces.
Obviously, it is always nice to save a few lines of code, but keep in mind that readability of your code is just as important.

Here is a challenge: create a comprehension for three-dimensional list, resulting in a single list with every item (i) of the 3 sub-lists.

------------------------------------------------------------------------------

Set Comprehensions

Set comprehensions follow the same format as ordered [list] comprehensions except that {sets} are unordered.
All of the rules above, including adding conditionals and nesting comprehension, apply to sets as well.
Another challenge: reproduce some of the exercises above with set comprehensions.

------------------------------------------------------------------------------

Dict Comprehensions

Dictionary comprehensions may look scary, but they are simple once you understand the notation.
They are similar to list and set comprehension, but with one difference:
*You must keep track of both the dict's keys and values.
EX #5:
old_dict = {1: 100, 2: 200, 3: 300}
new_dict = {key*2: value for key, value in dict.items()}

PYTHON'S PROCESS:
1. Iterate through each (key: value) pair of the old_dict, multiplying (key) by (2).
2. Appends each (key: value) pair to new_dict.

Thus, the resulting newdict1 would be {2: 100, 4: 200, 6: 300}.

Likewise, we can also iterate over and change dictionary's values, iterate over and change both its keys and values, and add conditional statements just as we did with lists and sets:

Can you guess what print(newdict1), print(newdict2), and print(newdict3) produce?

newdict2 = {k: v*2 for k, v in old_dict.items()}
newdict3 = {k*2: v*2 for k, v in old_dict.items()}
newdict4 = {k: v for k, v in old_dict.items() if k*v < 500}

------------------------------------------------------------------------------

Tuple Comprehensions (Advanced)

Tuple comprehensions are unusual in that they do not produce tuples, but rather generators (a type of iterator).

EX #6:
old_tuple = (1, 2, 3)
new_generator = (i for i in t)

Try calling the below statements in the Python console:
print(new_generator)
for i in new_generator:
    print(i)

Notice: -Printing "new_generator" prints something like "<generator object at 0x8158db8>".
-Looping through the new_generator prints each item once.

To actually create a tuple out of tuple comprehension, call a tuple explicitly on the comprehension:

SPECIAL FORMAT:
newtuple = tuple(i for i in t)

------------------------------------------------------------------------------

Putting It All Together

By now, you have learned how to read and use list, set, and dict comprehensions in basic forms.
Let's consider different and interesting ways!
We will be using the variables below throughout this section:

old_list = [1, 2, 3, 4]
old_set = {'a', 'e', 'i', 'o', 'u'}
old_dict = {'X': 10, 'Y': 20, 'Z': 25}

We can, for example, print a list of the items in old_set:
print([i for i in old_set])

We can also print a list of keys in old_dict:
print([k for k in old_dict.keys()])

We can also combine the values in old_dict and with the values in old_list.
Can you guess what the comprehension below produce?

print([i*v for i in old_list for v in old_dict.values()])

What about these comprehensions?

print({n for n in old_list})
print([k+i for k in old_dict.keys() for i in old_set])
print([i*n for i in old_set for n in old_list])
print([i*n for n in old_list for i in old_set])
print([i for i in old_list if i%2 == 0 else 'X'])

Understanding comprehensions takes a lot of practice and experimentation, so don't worry if the examples above seem tough!
The bottom line: comprehensions can be very handy if used properly, helping you save both time and space in your program. As a result, you will have shorter, cleaner, and more precise code.