Lab 4: Sequences, Mutability, Trees

Due by 11:59pm on Thursday, July 10.

Starter Files

Download lab04.zip.

Required Questions


Getting Started Videos

These videos may provide some helpful direction for tackling the coding problems on this assignment.

To see these videos, you should be logged into your berkeley.edu email.

YouTube link

Sequences

Consult the drop-downs if you need a refresher on lists, list comprehensions, for loops, or ranges. It's okay to skip directly to the questions and refer back here should you get stuck.

Lists

A list is a data structure that can hold an ordered collection of items. These items, known as elements, can be of any data type, including numbers, strings, or even other lists. A comma-separated list of expressions in square brackets creates a list:

>>> list_of_values = [2, 1, 3, True, 3]
>>> nested_list = [2, [1, 3], [True, [3]]]

Each position in a list has an index, with the left-most element indexed 0.

>>> list_of_values[0]
2
>>> nested_list[1]
[1, 3]

A negative index counts from the end, with the right-most element indexed -1.

>>> nested_list[-1]
[True, [3]]

Adding lists creates a longer list containing the elements of the added lists.

>>> [1, 2] + [3] + [4, 5]
[1, 2, 3, 4, 5]

List Comprehensions

A list comprehension describes the elements in a list and evaluates to a new list containing those elements.

There are two forms:

[<expression> for <element> in <sequence>]
[<expression> for <element> in <sequence> if <conditional>]

Here's an example that starts with [1, 2, 3, 4], picks out the even elements 2 and 4 using if i % 2 == 0, then squares each of these using i*i. The purpose of for i is to give a name to each element in [1, 2, 3, 4].

>>> [i*i for i in [1, 2, 3, 4] if i % 2 == 0]
[4, 16]

This list comprehension evaluates to a list of:

  • The value of i*i
  • For each element i in the sequence [1, 2, 3, 4]
  • For which i % 2 == 0

In other words, this list comprehension will create a new list that contains the square of every even element of the original list [1, 2, 3, 4].

We can also rewrite a list comprehension as an equivalent for statement, such as for the example above:

>>> result = []
>>> for i in [1, 2, 3, 4]:
...     if i % 2 == 0:
...         result = result + [i*i]
>>> result
[4, 16]

for Statements

A for statement executes code for each element of a sequence, such as a list or range. Each time the code is executed, the name right after for is bound to a different element of the sequence.

for <name> in <expression>:
    <suite>

First, <expression> is evaluated. It must evaluate to a sequence. Then, for each element in the sequence in order,

  1. <name> is bound to the element.
  2. <suite> is executed.

Here is an example:

for x in [-1, 4, 2, 0, 5]:
    print("Current elem:", x)

This would display the following:

Current elem: -1
Current elem: 4
Current elem: 2
Current elem: 0
Current elem: 5

Ranges

A range is a data structure that holds integer sequences. A range can be created by:

  • range(stop) contains 0, 1, ..., stop - 1
  • range(start, stop) contains start, start + 1, ..., stop - 1

Notice how the range function doesn't include the stop value; it generates numbers up to, but not including, the stop value.

For example:

>>> for i in range(3):
...     print(i)
...
0
1
2

While ranges and lists are both sequences, a range object is different from a list. A range can be converted to a list by calling list():

>>> range(3, 6)
range(3, 6)  # this is a range object
>>> list(range(3, 6))
[3, 4, 5]  # list() converts the range object to a list
>>> list(range(5))
[0, 1, 2, 3, 4]
>>> list(range(1, 6))
[1, 2, 3, 4, 5]

List Mutation Operations

There are many built-in methods that we can use to mutate lists. Here are some of the most useful ones:

  • append(el): Appends el to the end of the list and returns None.
  • extend(lst): Extends the list by concatenating it with lst and returns None.
  • remove(el): Removes the first occurence of el from the list and returns None.
  • insert(i, el): Inserts el at index i and returns None.
  • pop(i): Removes and returns the element at index i. If no index is given, removes and returns the last element in the list.

Let's see these methods in action:

>>> l = [3, 5, 6]
>>> l.append(10)    # Append 10 to the end of the list
>>> l
[3, 5, 6, 10]
>>> l.extend([30, 40])
>>> l
[3, 5, 6, 10, 30, 40]
>>> l.remove(5)     # Remove the first occurrence of 5
>>> l
[3, 6, 10, 30, 40]
>>> l.insert(2, -2) # Insert -2 at index 2
>>> l
[3, 6, -2, 10, 30, 40]
>>> l.pop()         # Remove and return the last element
40
>>> l
[3, 6, -2, 10, 30]
>>> l.pop(2)        # Remove and return the element at index 2
-2
>>> l
[3, 6, 10, 30]

Take note of two things:

  • The name l refers to the same list object during this entire session; it is never reassigned. The reason the output looks different each time we call a method is because the list that l evaluates to is being mutated.
  • The only method here that has a return value is pop! All of the other methods return None.
Here are the above method calls displayed in an environment diagram:

is and ==

We have seen how to use the == operator to check if two expressions evaluate to equal values. We now introduce a new comparison operator, is, that checks whether two expressions evaluate to the same values.

Wait, what's the difference? For primitive values, there is none:

>>> 2 + 2 == 3 + 1
True
>>> 2 + 2 is 3 + 1
True

This is because all primitives have the same identity under the hood. However, with non-primitive values, such as lists, each object has its own identity. That means you can construct two objects that may look exactly the same but have different identities.

>>> lst1 = [1, 2, 3, 4]
>>> lst2 = [1, 2, 3, 4]
>>> lst1 == lst2
True
>>> lst1 is lst2
False

Here, although the lists referred to by lst1 and lst2 have equal contents, they are not the same object. In other words, they are the same in terms of equality, but not in terms of identity.

This is important in our discussion of mutability because when we mutate an object, we simply change its state, not its identity.

>>> lst1 = [1, 2, 3, 4]
>>> lst2 = lst1
>>> lst1.append(5)
>>> lst2
[1, 2, 3, 4, 5]
>>> lst1 is lst2
True

Q1: WWPD: Lists, Sequences, and Mutation

Important: For all WWPD questions, type Function if you believe the answer is <function...>, Error if it errors, and Nothing if nothing is displayed.

Use Ok to test your knowledge with the following "What Would Python Display?" questions:

python3 ok -q lists-sequences-mutation -u

>>> s = [7//3, 5, [4, 0, 1], 2]
>>> s[0]
______
2
>>> s[2]
______
[4, 0, 1]
>>> s[-1]
______
2
>>> len(s)
______
4
>>> 4 in s
______
False
>>> 4 in s[2]
______
True
>>> s[2] + [3 + 2]
______
[4, 0, 1, 5]
>>> 5 in s[2]
______
False
>>> s[2] * 2
______
[4, 0, 1, 4, 0, 1]
>>> list(range(3, 6))
______
[3, 4, 5]
>>> range(3, 6)
______
range(3, 6)
>>> r = range(3, 6) >>> [r[0], r[2]]
______
[3, 5]
>>> range(4)[-1]
______
3
>>> [2 * x for x in range(4)]
______
[0, 2, 4, 6]
>>> [y for y in [6, 1, 6, 1] if y > 2]
______
[6, 6]
>>> [[1] + s for s in [[4], [5, 6]]]
______
[[1, 4], [1, 5, 6]]
>>> [z + 1 for z in range(10) if z % 3 == 0]
______
[1, 4, 7, 10]
>>> # If nothing would be output by Python, type Nothing
>>> # If the code would error, type Error
>>> s = [6, 7, 8]
>>> print(s.append(6))
______
None
>>> s
______
[6, 7, 8, 6]
>>> s.insert(0, 9) >>> s
______
[9, 6, 7, 8, 6]
>>> x = s.pop(1) >>> s
______
[9, 7, 8, 6]
>>> s.remove(x) >>> s
______
[9, 7, 8]
>>> a, b = s, s[:] >>> a is s
______
True
>>> b == s
______
True
>>> b is s
______
False
>>> a.pop()
______
8
>>> a + b
______
[9, 7, 9, 7, 8]
>>> s = [3] >>> s.extend([4, 5]) >>> s
______
[3, 4, 5]
>>> a
______
[9, 7]
>>> s.extend([s.append(9), s.append(10)]) >>> s
______
[3, 4, 5, 9, 10, None, None]

Q2: Insert Items

Write a function that takes in a list s, a value before, and a value after. It modifies s in place by inserting after just after each value equal to before in s. It returns s.

Important: No new lists should be created.

Note: If the values passed into before and after are equal, make sure you're not creating an infinitely long list while iterating through it. If you find that your code is taking more than a few seconds to run, the function may be in an infinite loop of inserting new values.

def insert_items(s, before, after):
    """Insert after into s following each occurrence of before and then return s.

    >>> test_s = [1, 5, 8, 5, 2, 3]
    >>> new_s = insert_items(test_s, 5, 7)
    >>> new_s
    [1, 5, 7, 8, 5, 7, 2, 3]
    >>> test_s
    [1, 5, 7, 8, 5, 7, 2, 3]
    >>> new_s is test_s
    True
    >>> double_s = [1, 2, 1, 2, 3, 3]
    >>> double_s = insert_items(double_s, 3, 4)
    >>> double_s
    [1, 2, 1, 2, 3, 4, 3, 4]
    >>> large_s = [1, 4, 8]
    >>> large_s2 = insert_items(large_s, 4, 4)
    >>> large_s2
    [1, 4, 4, 8]
    >>> large_s3 = insert_items(large_s2, 4, 6)
    >>> large_s3
    [1, 4, 6, 4, 6, 8]
    >>> large_s3 is large_s
    True
    """
    "*** YOUR CODE HERE ***"

Use Ok to test your code:

python3 ok -q insert_items

Dictionaries

Consult the drop-down if you need a refresher on dictionaries. It's okay to skip directly to the questions and refer back here should you get stuck.

A dictionary contains key-value pairs and allows the values to be looked up by their key using square brackets. Each key must be unique.

>>> d = {2: 4, 'two': ['four'], (1, 1): 4}
>>> d[2]
4
>>> d['two']
['four']
>>> d[(1, 1)]
4

The sequence of keys or values or key-value pairs can be accessed using .keys() or .values() or .items().

>>> for k in d.keys():
...     print(k)
...
2
two
(1, 1)
>>> for v in d.values():
...     print(v)
...
4
['four']
4
>>> for k, v in d.items():
...     print(k, v)
...
2 4
two ['four']
(1, 1) 4

By default, iterating through a dictionary iterates through its keys.

>>> for x in d:
...     print(x)
...
2
two
(1, 1)

You can check whether a dictionary contains a key using in:

>>> 'two' in d
True
>>> 4 in d
False

Attempting to access a key that does not exist in a dictionary will cause an error. You can use .get(<key>, <default>), which returns the value corresponding to <key> in a dictionary or, if it doesn't exist, returns <default>.

>>> d[3]
KeyError: 3
>>> d.get(3, "fun")
"fun"
>>> d.get(2, "fun")
4

The contents of a dictionary can be modified using =:

>>> d
{2: 4, 'two': ['four'], (1, 1): 4}
>>> d[(1, 1)] = "61a"
>>> d[(1, 1)]
'61a'

A dictionary comprehension is an expression that evaluates to a new dictionary.

>>> {3*x: 3*x + 1 for x in range(2, 5)}
{6: 7, 9: 10, 12: 13}

Q3: WWPD: Dictionaries

Important: For all WWPD questions, type Function if you believe the answer is <function...>, Error if it errors, and Nothing if nothing is displayed.

Use Ok to test your knowledge with the following "What Would Python Display?" questions:

python3 ok -q dicts -u

>>> pokemon = {'pikachu': 25, 'dragonair': 148, 'mew': 151}
>>> pokemon['pikachu']
______
25
>>> len(pokemon)
______
3
>>> pokemon['jolteon'] = 135 >>> pokemon['mew'] = 25 >>> len(pokemon)
______
4
>>> 'mewtwo' in pokemon
______
False
>>> 'pikachu' in pokemon
______
True
>>> 25 in pokemon
______
False
>>> 148 in pokemon.values()
______
True
>>> 151 in pokemon.keys()
______
False
>>> 'mew' in pokemon.keys()
______
True
>>> pokemon.get(151, 170)
______
170
>>> ('mew', 25) in pokemon.items()
______
True
>>> pokemon['ditto'] = pokemon['jolteon'] >>> pokemon['ditto']
______
135
>>> mystery = {i - 1: i**2 for i in range(5)}
>>> mystery[2]
______
9
>>> mystery[4]
______
Error

Q4: Group By

Write a function that takes in a list s and a function fn, and returns a dictionary that groups the elements of s based on the result of applying fn.

  • The dictionary should have one key for each unique result of applying fn to elements of s.
  • The value for each key should be a list of all elements in s that, when passed to fn, produce that key (what it evaluates to).

In other words, for each element e in s, determine fn(e) and add e to the list corresponding to fn(e) in the dictionary.

def group_by(s, fn):
    """Return a dictionary of lists that together contain the elements of s.
    The key for each list is the value that fn returns when called on any of the
    values of that list.

    >>> group_by([12, 23, 14, 45], lambda p: p // 10)
    {1: [12, 14], 2: [23], 4: [45]}
    >>> group_by(range(-3, 4), lambda x: x * x)
    {9: [-3, 3], 4: [-2, 2], 1: [-1, 1], 0: [0]}
    """
    grouped = {}
    for ____ in ____:
        key = ____
        if key in grouped:
            ____
        else:
            grouped[key] = ____
    return grouped

Use Ok to test your code:

python3 ok -q group_by

Trees

Consult the drop-down if you need a refresher on trees. It's okay to skip directly to the questions and refer back here should you get stuck.

Our tree abstract data type consists of a root and a list of its branches. To create a tree and access its root value and branches, use the following interface of constructor and selectors:

  • Constructor

    • tree(label, branches=[]): creates a tree object with the given label value at its root node and list of branches. Notice that the second argument to this constructor, branches, is optional — if you want to make a tree with no branches, leave this argument empty.
  • Selectors

    • label(tree): returns the value in the root node of tree.
    • branches(tree): returns the list of branches of the given tree (each of which is also a tree)
  • Convenience function

    • is_leaf(tree): returns True if tree's list of branches is empty, and False otherwise.

For example, the tree generated by

number_tree = tree(1,
         [tree(2),
          tree(3,
               [tree(4),
                tree(5)]),
          tree(6,
               [tree(7)])])

would look like this:

   1
 / | \
2  3  6
  / \  \
 4   5  7

To extract the number 3 from this tree, which is the label of the root of its second branch, we would do this:

label(branches(number_tree)[1])

Q5: WWPD: Trees

Use Ok to test your knowledge with the following "What Would Python Display?" questions:

python3 ok -q wwpd -u

Type Error if the code errors and Nothing if nothing is displayed. Draw out the tree on the board or a piece of paper if you get stuck!

>>> from lab04 import *
>>> t = tree(1, tree(2))
______
Error
>>> t = tree(1, [tree(2)])
______
Nothing
>>> label(t)
______
1
>>> label(branches(t)[0])
______
2
>>> x = branches(t) >>> len(x)
______
1
>>> is_leaf(x[0])
______
True
>>> branch = x[0] >>> label(t) + label(branch)
______
3
>>> len(branches(branch))
______
0
>>> from lab04 import *
>>> b1 = tree(5, [tree(6), tree(7)])
>>> b2 = tree(8, [tree(9, [tree(10)])])
>>> t = tree(11, [b1, b2])
>>> for b in branches(t):
...     print(label(b))
______
5 8
>>> for b in branches(t): ... print(is_leaf(branches(b)[0])) ...
______
True False
>>> [label(b) + 100 for b in branches(t)]
______
[105, 108]
>>> [label(b) * label(branches(b)[0]) for b in branches(t)]
______
[30, 72]

Q6: Pruning Leaves

Define a function prune_leaves that given a tree t and a tuple of values vals, produces a version of t with all its leaves that are in vals removed. Do not attempt to try to remove non-leaf nodes and do not remove leaves that do not match any of the items in vals. Return None if pruning the tree results in there being no nodes left in the tree.

def prune_leaves(t, vals):
    """Return a modified copy of t with all leaves that have a label
    that appears in vals removed.  Return None if the entire tree is
    pruned away.

    >>> t = tree(2)
    >>> print(prune_leaves(t, (1, 2)))
    None
    >>> numbers = tree(1, [tree(2), tree(3, [tree(4), tree(5)]), tree(6, [tree(7)])])
    >>> print_tree(numbers)
    1
      2
      3
        4
        5
      6
        7
    >>> print_tree(prune_leaves(numbers, (3, 4, 6, 7)))
    1
      2
      3
        5
      6
    """
    "*** YOUR CODE HERE ***"

Use Ok to test your code:

python3 ok -q prune_leaves

Data Abstraction

Consult the drop-down if you need a refresher on data abstraction. It's okay to skip directly to the questions and refer back here should you get stuck.

A data abstraction is a set of functions that compose and decompose compound values. One function called the constructor puts together two or more parts into a whole (such as a rational number; also known as a fraction), and other functions called selectors return parts of that whole (such as the numerator or denominator).

def rational(n, d):
    "Return a fraction n / d for integers n and d."

def numer(r):
    "Return the numerator of rational number r."

def denom(r):
    "Return the denominator of rational number r."

Crucially, one can use a data abstraction without knowing how these functions are implemented. For example, we (humans) can verify that mul_rationals is implemented correctly just by knowing what rational, numer, and denom do without knowing how they are implemented.

def mul_rationals(r1, r2):
    "Return the rational number r1 * r2."
    return rational(numer(r1) * numer(r2), denom(r1) * denom(r2))

However, for Python to run the program, the data abstraction requires an implementation. Using knowledge of the implementation crosses the abstraction barrier, which separates the part of a program that depends on the implementation of the data abstraction from the part that does not. A well-written program typically will minimize the amount of code that depends on the implementation so that the implementation can be changed later on without requiring much code to be rewritten.

When using a data abstraction that has been provided, write your program so that it will still be correct even if the implementation of the data abstraction changes.


Cities

Say we have a data abstraction for cities. A city has a name, a latitude coordinate, and a longitude coordinate.

Our data abstraction has one constructor:

  • make_city(name, lat, lon): Creates a city object with the given name, latitude, and longitude.

We also have the following selectors in order to get the information for each city:

  • get_name(city): Returns the city's name
  • get_lat(city): Returns the city's latitude
  • get_lon(city): Returns the city's longitude

Here is how we would use the constructor and selectors to create cities and extract their information:

>>> berkeley = make_city('Berkeley', 122, 37)
>>> get_name(berkeley)
'Berkeley'
>>> get_lat(berkeley)
122
>>> new_york = make_city('New York City', 74, 40)
>>> get_lon(new_york)
40

All of the selector and constructor functions can be found in the lab file if you are curious to see how they are implemented. However, the point of data abstraction is that, when writing a program about cities, we do not need to know the implementation.

Q7: Distance

We will now implement the function distance, which computes the distance between two city objects. Recall that the distance between two coordinate pairs (x1, y1) and (x2, y2) can be found by calculating the sqrt of (x1 - x2)**2 + (y1 - y2)**2. We have already imported sqrt for your convenience. Use the latitude and longitude of a city as its coordinates; you'll need to use the selectors to access this info!

from math import sqrt
def distance(city_a, city_b):
    """
    Returns the distance between city_a and city_b according to their
    coordinates.

    >>> city_a = make_city('city_a', 0, 1)
    >>> city_b = make_city('city_b', 0, 2)
    >>> distance(city_a, city_b)
    1.0
    >>> city_c = make_city('city_c', 6.5, 12)
    >>> city_d = make_city('city_d', 2.5, 15)
    >>> distance(city_c, city_d)
    5.0
    """
    "*** YOUR CODE HERE ***"

Use Ok to test your code:

python3 ok -q distance

Q8: Closer City

Next, implement closer_city, a function that takes a latitude, longitude, and two cities, and returns the name of the city that is closer to the provided latitude and longitude.

You may only use the selectors get_name get_lat get_lon, constructors make_city, and the distance function you just defined for this question.

Hint: How can you use your distance function to find the distance between the given location and each of the given cities?

def closer_city(lat, lon, city_a, city_b):
    """
    Returns the name of either city_a or city_b, whichever is closest to
    coordinate (lat, lon). If the two cities are the same distance away
    from the coordinate, consider city_b to be the closer city.

    >>> berkeley = make_city('Berkeley', 37.87, 112.26)
    >>> stanford = make_city('Stanford', 34.05, 118.25)
    >>> closer_city(38.33, 121.44, berkeley, stanford)
    'Stanford'
    >>> bucharest = make_city('Bucharest', 44.43, 26.10)
    >>> vienna = make_city('Vienna', 48.20, 16.37)
    >>> closer_city(41.29, 174.78, bucharest, vienna)
    'Bucharest'
    """
    "*** YOUR CODE HERE ***"

Use Ok to test your code:

python3 ok -q closer_city

Q9: Don't violate the abstraction barrier!

Note: this question has no code-writing component (if you implemented the previous two questions correctly).

When writing functions that use a data abstraction, we should use the constructor(s) and selector(s) whenever possible instead of assuming the data abstraction's implementation. Relying on a data abstraction's underlying implementation is known as violating the abstraction barrier.

It's possible that you passed the doctests for the previous questions even if you violated the abstraction barrier. To check whether or not you did so, run the following command:

Use Ok to test your code:

python3 ok -q check_city_abstraction

The check_city_abstraction function exists only for the doctest, which swaps out the implementations of the original abstraction with something else, runs the tests from the previous two parts, then restores the original abstraction.

The nature of the abstraction barrier guarantees that changing the implementation of a data abstraction should not affect the functionality of any programs that use that data abstraction, as long as the constructors and selectors were used properly.

If you passed the Ok tests for the previous questions but not this one, the fix is simple! Just replace any code that violates the abstraction barrier with the appropriate constructor or selector.

Make sure that your functions pass the tests with both the first and the second implementations of the data abstraction and that you understand why they should work for both before moving on.

Check Your Score Locally

You can locally check your score on each question of this assignment by running

python3 ok --score

This does NOT submit the assignment! When you are satisfied with your score, submit the assignment to Gradescope to receive credit for it.

Submit Assignment

Submit this assignment by uploading any files you've edited to the appropriate Gradescope assignment. Lab 00 has detailed instructions. Correctly completing all questions is worth one point for regular section students and two points for mega section students.

If you are in regular section, be sure to fill out your TA's attendance form before you leave section. Attending lab section is worth one point for regular section students.

Optional Questions

Q10: Buying Fruit

Implement the buy function that takes three parameters:

  1. fruits_to_buy: A list of strings representing the fruits you need to buy. At least one of each fruit must be bought.
  2. prices: A dictionary where the keys are fruit names (strings) and the values are positive integers representing the cost of each fruit.
  3. total_amount: An integer representing the total money available for purchasing the fruits. Take a look at the docstring for more details on the input structure.

The function should print all possible ways to buy the required fruits so that the combined cost equals total_amount. You can only select fruits mentioned in fruits_to_buy list.

Note: You can use the display function to format the output. Call display(fruit, count) for each fruit and its corresponding quantity to generate a string showing the type and amount of fruit bought.

Hint: How can you ensure that every combination includes at least one of each fruit listed in fruits_to_buy?

def buy(fruits_to_buy, prices, total_amount):
    """Print ways to buy some of each fruit so that the sum of prices is amount.

    >>> prices = {'oranges': 4, 'apples': 3, 'bananas': 2, 'kiwis': 9}
    >>> buy(['apples', 'oranges', 'bananas'], prices, 12)  # We can only buy apple, orange, and banana, but not kiwi
    [2 apples][1 orange][1 banana]
    >>> buy(['apples', 'oranges', 'bananas'], prices, 16)
    [2 apples][1 orange][3 bananas]
    [2 apples][2 oranges][1 banana]
    >>> buy(['apples', 'kiwis'], prices, 36)
    [3 apples][3 kiwis]
    [6 apples][2 kiwis]
    [9 apples][1 kiwi]
    """
    def add(fruits, amount, cart):
        if fruits == [] and amount == 0:
            print(cart)
        elif fruits and amount > 0:
            fruit = fruits[0]
            price = ____
            for k in ____:
                # Hint: The display function will help you add fruit to the cart.
                add(____, ____, ____)
    add(fruits_to_buy, total_amount, '')

def display(fruit, count):
    """Display a count of a fruit in square brackets.

    >>> display('apples', 3)
    '[3 apples]'
    >>> display('apples', 1)
    '[1 apple]'
    >>> print(display('apples', 3) + display('kiwis', 3))
    [3 apples][3 kiwis]
    """
    assert count >= 1 and fruit[-1] == 's'
    if count == 1:
        fruit = fruit[:-1]  # get rid of the plural s
    return '[' + str(count) + ' ' + fruit + ']'

Use Ok to test your code:

python3 ok -q buy

Q11: Count Palindromes

The Python library defines filter, map, and reduce, which operate on Python sequences. Devise a function that counts the number of palindromic words (those that read the same backwards as forwards) in a tuple of words using only lambda, basic operations on strings, the tuple constructor, conditional expressions, and the functions filter, map, and reduce. Specifically, do not use recursion or any kind of loop:

def count_palindromes(L):
    """The number of palindromic words in the sequence of strings
    L (ignoring case).

    >>> count_palindromes(("Acme", "Madam", "Pivot", "Pip"))
    2
    """
    return ______

Hint: The easiest way to get the reversed version of a string s is to use the Python slicing notation trick s[::-1]. Also, the function lower, when called on strings, converts all of the characters in the string to lowercase. For instance, if the variable s contains the string "PyThoN", the expression s.lower() evaluates to "python".

Use Ok to test your code:

python3 ok -q count_palindromes