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) or option (mac) on the keyboard and use the mouse to click on tests/test_list_comprehensions.py:7 to open it in the editor

  • then change True to False 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 name

  • iterable = 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 from 0 to 3

  • for item in iterable: goes over every item in the range object

  • a_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 in iterable leaves a remainder of 0 when divided by 2

  • % 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 them

  • I 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 in get_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 write x raised to the power of y

    \[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

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?


data structures: List Comprehensions: tests and solutions