lists: list comprehensions¶
List Comprehensions are a simple way to make a list from an iterable with one line
requirements¶
I open a terminal to run makePythonTdd.sh with
list_comprehensions
as the name of the project./makePythonTdd.sh list_comprehensions
on Windows without Windows Subsystem Linux use makePythonTdd.ps1
./makePythonTdd.ps1 list_comprehensions
it makes the folders and files that are needed, installs packages, runs the first test, and the terminal shows AssertionError
E AssertionError: True is not false tests/test_list_comprehensions.py:7: AssertionError
I hold
ctrl
(windows/linux) oroption
(mac) on the keyboard and use the mouse to click ontests/test_list_comprehensions.py:7
to open it in the editorthen change
True
toFalse
to make the test pass
test_making_a_list_w_a_for_loop¶
red: make it fail¶
I change test_failure
to test_making_a_list_w_a_for_loop
import unittest
class TestListComprehensions(unittest.TestCase):
def test_making_a_list_w_a_for_loop(self):
a_list = []
iterable = range(0, 4)
for item in iterable:
a_list.append(item)
self.assertEqual(a_list, [])
the terminal shows AssertionError
AssertionError: Lists differ: [0, 1, 2, 3] != []
a_list = []
makes an empty list and gives it a nameiterable = range(0, 4)
makes a range object that goes from the first given number to the second given number minus 1, in this case it goes from0
to3
for item in iterable:
goes over every item in the range objecta_list.append(item)
gets called every time the for loop runs
the list is no longer empty after the operation
green: make it pass¶
I change the expectation to match the values in the terminal
self.assertEqual(a_list, [0, 1, 2, 3])
the test passes
refactor: make it better¶
I add another assertion to show that I can get the same result with the list constructor
self.assertEqual(a_list, [0, 1, 2, 3]) self.assertEqual(a_list, list())
the terminal shows AssertionError
AssertionError: Lists differ: [0, 1, 2, 3] != []
I add the iterable to the call
self.assertEqual(a_list, list(iterable))
the test passes. Why use a for loop when I can use the list constructor to get the same thing? Sometimes one is better than the other
I add another assertion to practice writing a for loop
self.assertEqual(a_list, list(iterable)) self.assertEqual( src.list_comprehensions.for_loop(iterable), [0, 1, 2, 3] )
the terminal shows NameError
NameError: name 'src' is not defined
I add it to the list of Exceptions encountered
# Exceptions Encountered # AssertionError # NameError
I add an import statement
import src.list_comprehensions import unittest
the terminal shows AttributeError
AttributeError: module 'src.list_comprehensions' has no attribute 'for_loop'
I add it to the list of Exceptions encountered
# Exceptions Encountered # AssertionError # NameError # AttributeError
I add a function to
list_comprehensions.py
def for_loop(): return None
the terminal shows TypeError
TypeError: for_loop() takes 0 positional arguments but 1 was given
I add the argument
def for_loop(iterable): return None
the terminal shows AssertionError
AssertionError: None != [0, 1, 2, 3]
I change the return statement
def for_loop(iterable): return [0, 1, 2, 3]
the test passes
I need a better test, this one breaks when I change the values in the range object
iterable = range(0, 5)
the terminal shows AssertionError
AssertionError: Lists differ: [0, 1, 2, 3, 4] != [0, 1, 2, 3]
I undo the change then import the random module
import random import src.list_comprehensions import unittest
I change the second value given to the range object
iterable = range(0, random.randint(2, 1000))
the terminal shows AssertionError
AssertionError: Lists differ: [0, 1, 2, 3, ...] != [0, 1, 2, 3]
the values now change every time the test runs
I change the expectation in the first assertion
self.assertEqual(a_list, list(iterable)) self.assertEqual(a_list, list(iterable))
the test passes. Since it is a duplication I remove the line
I change the expectation of the second assertion
self.assertEqual(a_list, list(iterable)) self.assertEqual( src.list_comprehensions.for_loop(iterable), a_list )
the terminal shows AssertionError
AssertionError: Lists differ: [0, 1, 2, 3] != [0, 1, 2, 3, 4, ...]
I change the return statement in the function
def for_loop(iterable): return list(iterable)
the test passes, but I made this function to practice writing a for loop so I change it
def for_loop(iterable): result = [] for item in iterable: result.append(item) return result
the test is still green
test_making_a_list_w_extend¶
I can also use the extend method to make a list from an iterable
red: make it fail¶
I add a new test
def test_making_a_list_w_a_for_loop(self):
...
def test_making_a_list_w_extend(self):
a_ list = []
iterable = range(0, random.randint(2, 1000))
self.assertIsNone(a_list.extend())
the terminal shows TypeError
TypeError: list.extend() takes exactly one argument (0 given)
green: make it pass¶
I add the iterable
self.assertIsNone(a_list.extend(iterable))
the terminal shows green again
refactor: make it better¶
I add another assertion to see what changed in the list
self.assertIsNone(a_list.extend(iterable)) self.assertEqual( a_list, [] )
the terminal shows AssertionError
AssertionError: Lists differ: [0, 1, 2, 3, 4, ...] != []
I change the expectation
self.assertEqual( a_list, src.list_comprehensions.for_loop(iterable) )
the test passes. This way uses less lines than the for loop
I made the same variables twice, one for the empty list and another for the iterable, I add them to the setUp method to remove duplication and change the tests to use the class variables
class TestListComprehensions(unittest.TestCase): def setUp(self): self.a_list = [] self.iterable = range(0, random.randint(2, 1000)) def test_making_a_list_w_a_for_loop(self): for item in self.iterable: self.a_list.append(item) self.assertEqual(self.a_list, list(self.iterable)) self.assertEqual( src.list_comprehensions.for_loop(self.iterable), self.a_list ) def test_making_a_list_w_extend(self): self.assertIsNone(self.a_list.extend(self.iterable)) self.assertEqual( self.a_list, src.list_comprehensions.for_loop(self.iterable) )
the terminal still shows green
test_making_a_list_w_a_list_comprehension¶
I can also use a list comprehension to make a list from an iterable
red: make it fail¶
I add a failing test
def test_making_a_list_w_extend(self):
...
def test_making_a_list_w_a_list_comprehension(self):
self.assertEqual(
src.list_comprehensions.for_loop(self.iterable),
[]
)
the terminal shows AssertionError
AssertionError: Lists differ: [0, 1, 2, 3, ...] != []
green: make it pass¶
I add a list comprehension
self.assertEqual(
src.list_comprehensions.for_loop(self.iterable),
# list(self.iterable)
[item for item in self.iterable]
)
the test passes
refactor: make it better¶
I add another assertion for practice
self.assertEqual( src.list_comprehensions.for_loop(self.iterable), [item for item in self.iterable] ) self.assertEqual( src.list_comprehensions.list_comprehension(self.iterable), [item for item in self.iterable] )
the terminal shows AttributeError
AttributeError: module 'src.list_comprehensions' has no attribute 'list_comprehension'
I add the function
def for_loop(iterable): ... def list_comprehension(iterable): return [item for item in iterable]
the test passes. The list comprehension has the same syntax as the for loop
I made 2 functions - one that uses a for loop and another that uses a list comprehension to do the same thing
result = [] for item in iterable: result.append()
and
[item for item in iterable]
the difference between them is that in the first case I have to
with the list comprehension, I do all the steps in one line, yet none of the other ways are better than using the list constructor, yet.
test_making_a_list_w_conditions¶
What if I had to build a list from an iterable based on a condition, how would I do it with the list constructor without changing the iterable? This is where a for loop or list comprehension works better
red: make it fail¶
I add a failing test
def test_making_a_list_w_conditions(self):
even_numbers = []
for item in self.iterable:
if item % 2 == 0:
even_numbers.append(item)
self.assertEqual(even_numbers, list(self.iterable))
the terminal shows AssertionError
AssertionError: Lists differ: [0, 2, 4, 6, 8, ...] != [0, 1, 2, 3, 4, 5, 6, 7, 8...]
if item % 2 == 0:
checks if the item initerable
leaves a remainder of0
when divided by2
%
is the modulo operator, which divides the number on the left by the number on the right and returns a remainder, there’s a test for it in test_the_modulo_operation
green: make it pass¶
I change the expectation to use a list comprehension
self.assertEqual(
even_numbers,
[item for item in self.iterable]
)
the terminal still shows AssertionError
AssertionError: Lists differ: [0, 2, 4, 6, 8, ...] != [0, 1, 2, 3, 4, 5, 6, 7, 8...]
I add the condition
self.assertEqual(
even_numbers,
[item for item in self.iterable if item % 2 == 0]
)
the test passes
refactor: make it better¶
I add another assertion for practice
self.assertEqual( even_numbers, [item for item in self.iterable if item % 2 == 0] ) self.assertEqual( src.list_comprehensions.get_even_numbers(self.iterable), [item for item in self.iterable if item % 2 == 0] )
the terminal shows AttributeError
AttributeError: module 'src.list_comprehensions' has no attribute 'get_even_numbers'
I add a function to
list_comprehensions.py
def list_comprehension(iterable): ... def get_even_numbers(iterable): return [item for item in iterable if item % 2 == 0]
the test passes
I wrote the same condition in the test 3 times. I add a function to remove the duplication
import unittest def condition(number): return number % 2 == 0
I change the condition in the test to reference the new function
def test_making_a_list_w_conditions(self): even_numbers = [] for item in self.iterable: # if item % 2 == 0: if condition(item): even_numbers.append(item)
the terminal still shows green. I remove the commented line and do the same thing in the first assertion
self.assertEqual( even_numbers, # [item for item in self.iterable if item % 2 == 0] [item for item in self.iterable if condition(item)] )
still green. I do it again with the next one
self.assertEqual( even_numbers, [item for item in self.iterable if condition(item)] ) self.assertEqual( src.list_comprehensions.get_even_numbers(self.iterable), # [item for item in self.iterable if item % 2 == 0] [item for item in self.iterable if condition(item)] )
the terminal still shows green. I do NOT recommend using
condition
as a name for a function because it is too general, it does not tell what it does. I use it to show that I think of a list comprehension as[item for item in iterable if condition]
.I add a new empty list to test another condition
def test_making_a_list_w_conditions(self): even_numbers, odd_numbers = [], [] for item in self.iterable: ...
even_numbers, odd_numbers = [], []
makes 2 empty lists and names themI add an else clause in the for loop
for item in self.iterable: if condition(item): even_numbers.append(item) else: odd_numbers.append(item)
I add an assertion
self.assertEqual( src.list_comprehensions.get_even_numbers(self.iterable), [item for item in self.iterable if condition(item)] ) self.assertEqual( odd_numbers, [item for item in self.iterable] )
the terminal shows AssertionError
AssertionError: Lists differ: [1, 3, 5, 7, ...] != [0, 1, 2, 3, 4, 5, 6, 7, 8, ...]
I add the condition to the assertion
self.assertEqual( odd_numbers, [item for item in self.iterable if not condition(item)] )
the test passes. I add another assertion
self.assertEqual( odd_numbers, [item for item in self.iterable if not condition(item)] ) self.assertEqual( src.list_comprehensions.get_odd_numbers(self.iterable), [item for item in self.iterable if not condition(item)] )
the terminal shows AttributeError
AttributeError: module 'src.list_comprehensions' has no attribute 'get_odd_numbers'. Did you mean: 'get_even_numbers'?
I add the function
def get_even_numbers(iterable): ... def get_odd_numbers(iterable): return [item for item in iterable if item % 2 != 0]
the test passes
I add a function for the condition in
list_comprehensions.py
and use a more descriptive name then call it inget_even_numbers
def list_comprehension(iterable): ... def is_even(number): return number % 2 == 0 def get_even_numbers(iterable): return [item for item in iterable if is_even(item)] return [item for item in iterable if item % 2 == 0]
the test is still passing. I remove the second return statement then call the new function in
get_odd_numbers
def get_even_numbers(iterable): ... def get_odd_numbers(iterable): return [item for item in iterable if not is_even(item)] return [item for item in iterable if item % 2 != 0]
the terminal still shows green, I remove the second return statement
test_making_a_list_w_processes¶
red: make it fail¶
I add a test to show I can perform operations in a list comprehension
def test_making_a_list_w_conditions(self):
...
def test_making_a_list_w_processes(self):
squares = []
for item in self.iterable:
squares.append(item*item)
self.assertEqual(
squares,
[item for item in self.iterable]
)
the terminal shows AssertionError
AssertionError: Lists differ: [0, 1, 4, 9, ...] != [0, 1, 2, 3, ...]
green: make it pass¶
I add the calculation to the list comprehension
self.assertEqual(
squares,
[item*item for item in self.iterable]
)
the test passes
refactor: make it better¶
I add another assertion
self.assertEqual( squares, [item*item for item in self.iterable] ) self.assertEqual( src.list_comprehensions.square(self.iterable), [item*item for item in self.iterable] )
the terminal shows AttributeError
AttributeError: module 'src.list_comprehensions' has no attribute 'square'
I add the function
def get_odd_numbers(iterable): ... def square(iterable): return [item**2 for item in iterable]
the test passes.
x**y
is how to writex
raised to the power ofy
\[x ^ y\]I add a function for the calculation I did 3 times in this test
import unittest def process(number): return number ** 2
I call it in the test
def test_making_a_list_w_processes(self): squares = [] for item in self.iterable: # squares.append(item*item) squares.append(process(item)) self.assertEqual( squares, # [item*item for item in self.iterable] [process(item) for item in self.iterable] ) self.assertEqual( src.list_comprehensions.square(self.iterable), # [item*item for item in self.iterable] [process(item) for item in self.iterable] )
the terminal still shows green. I remove the commented lines. I do NOT recommend using
process
as a name for a function because it is too general, it does not tell what it does. I use it to show that I think of a list comprehension as[process(item) for item in iterable]
test_making_a_list_w_processes_and_conditions¶
I can use both processes and conditions in a list comprehension
red: make it fail¶
I add a failing test
def test_making_a_list_w_processes(self):
...
def test_making_a_list_w_processes_and_conditions(self):
even_squares = [], odd_squares = []
for item in self.iterable:
if condition(item):
even_numbers.append(process(item))
else:
odd_numbers.append(process(item))
self.assertEqual(
even_squares,
[item for item in self.iterable]
)
the terminal shows AssertionError
AssertionError: Lists differ: [0, 4, 16, 36, ...] != [0, 1, 2, 3, 4, ...]
green: make it pass¶
I add a call to condition
self.assertEqual(
even_squares,
[item for item in self.iterable if condition(item)]
)
the terminal shows AssertionError
AssertionError: Lists differ: [0, 4, 16, 36, ...] != [0, 2, 4, 6, ...]
I add a call to process
self.assertEqual(
even_squares,
[process(item) for item in self.iterable if condition(item)]
)
the test passes
refactor: make it better¶
I add another assertion
self.assertEqual(
odd_squares,
[item for item in self.iterable]
)
the terminal shows AssertionError
AssertionError: Lists differ: [1, 9, 25, 49, ...] != [0, 1, 2, 3, 4, 5, 6, 7, ...]
I add a call to condition
self.assertEqual(
odd_squares,
[item for item in self.iterable if not condition(item)]
)
the terminal shows AssertionError
AssertionError: Lists differ: [1, 9, 25, 49, ...] != [1, 3, 5, 7, ...]
I add a call to processes
self.assertEqual(
odd_squares,
[process(item) for item in self.iterable if not condition(item)]
)
the test passes
review¶
The tests show I can make a list from an iterable with
a for loop
the list constructor
I can use functions and conditions with list comprehensions to make a list with one line
I can also do this with dictionaries, the syntax for a dict comprehension is any variation of the following
{a_process(key): another_process(value) for key/value in iterable if condition/NOT condition}
Would you like to test dictionaries?