We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Precise Type Hints with Literals and Generics step 15 of 26
Accept Read-Only Inputs and Return Concrete Results
Write two helpers. group_by splits items into groups by a key, and
merge_counts adds up several tables of counts. An earlier exercise in this
track showed that read-only types such as Sequence and Mapping accept more
callers than list and dict. Here you use read-only types for the inputs,
and return a plain dict that callers can change.
group_by(["pear", "plum", "apple"], lambda word: word[:1])
# {"p": ["pear", "plum"], "a": ["apple"]}
merge_counts(({"a": 2, "b": -1}, {"a": -2}))
# {"a": 0, "b": -1}
Pick each input type by what the function does with it
Iterable[T] says only that you can loop over the value. That is all both
helpers need, and it accepts a list, a tuple or a generator. Mapping[K, V]
lets you read keys and values, and it accepts a dict or a read-only mapping.
Return dict and list, because you build them and the caller may want to
change them.
What to write
from collections.abc import Callable, Iterable, Mapping
def group_by[T, K](items: Iterable[T], key: Callable[[T], K]) -> dict[K, list[T]]:
...
def merge_counts(sources: Iterable[Mapping[str, int]]) -> dict[str, int]:
...
group_by:
- Loop over
itemsonce, and callkeyonce per item. - Put each item in the group for its key. Equal keys share a group.
- List the groups in the order their keys first appear. Inside each group, keep the items in input order, duplicates included. Do not sort.
- Return a new dictionary of new lists that hold the original items. Empty
input gives
{}. - Do not catch exceptions from
keyor from the loop.
merge_counts:
- Read each source once, and add its values into a new dictionary.
- Keep keys whose total is zero or negative.
- Leave the sources unchanged. Empty input gives
{}. - Do not add
HOUSE_COUNTShere.solveadds it.
The starter defines HOUSE_COUNTS as a read-only mapping:
HOUSE_COUNTS: Final[Mapping[str, int]] = MappingProxyType({"z": 1})
Iterable[dict[str, int]] would reject it, because a Mapping is not a
dict. Iterable[Mapping[str, int]] accepts it, along with a generator of
mappings.
Keep both type parameters on group_by. Then mypy knows that grouping strings
by len gives dict[int, list[str]].
What solve returns
solve(words, counts, trap) returns a tuple of four values:
- The group keys from
group_by(words, lambda word: word[:1]), in first-seen order.word[:1]gives""for an empty word, whereword[0]would raiseIndexError. - The matching lists of words, in input order with duplicates.
merge_counts([*counts, HOUSE_COUNTS]). This addsHOUSE_COUNTSonce, even whencountsis empty, so the total for"z"goes up by 1.- The distinct characters of
trapin first-seen order, fromgroup_by(trap, lambda char: char). A string is iterable, sogroup_by("banana", ...)visits one character at a time.
Leave words, counts and HOUSE_COUNTS unchanged.
solve(["", "pear", "plum"], [{"z": -1}], "banana")
# (["", "p"], [[""], ["pear", "plum"]], {"z": 0}, ["b", "a", "n"])
solve([], [], "")
# ([], [], {"z": 1}, [])
The tests check the order of values 1, 2 and 4. They compare the merged dictionary by its keys and values only.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.