This solved function collects the words of a sentence by their length. The one interesting line is the one that avoids an if.
def words_by_length(sentence):
groups = {}
for word in sentence.split():
groups.setdefault(len(word), []).append(word)
return groups
# words_by_length('the quick brown fox')
# -> {3: ['the', 'fox'], 5: ['quick', 'brown']}
- setdefault returns the value, which is why it chains
groups.setdefault(k, []) inserts an empty list if k is missing and — either way — hands back the list that is now stored under k. Because it returns that list, .append can be called straight onto it. The long form is three lines with an if; this is one, and there is no branch to get backwards.
- The empty list is built every time, and that is fine here
setdefault evaluates its second argument whether or not it is needed, so a fresh [] is created on every loop and thrown away when the key already exists. For a literal that is free. If the default were expensive to build, collections.defaultdict — which only calls the factory on a miss — would be the better tool.
- Groups appear in first-seen order
Dictionaries have kept insertion order since Python 3.7, so the groups come out in the order their keys were first encountered, and the members of each group in the order they were read. Neither is an accident you should rely on without knowing it is guaranteed — it is.
You are building a dict where every value is a list. Walk the records once and, for each one, work out which list it belongs in and put it there.
The awkward part is the first record of each group, when the list does not exist yet. dict.setdefault(value, []) solves exactly that — read the worked example again if it has not clicked.
groups = {}. For each record: read record[key] — which raises KeyError by itself when the field is missing, which is what the contract asks for — then groups.setdefault(that_value, []).append(record). Return groups. Never modify record.
def group_by(records, key): groups = {} for record in records: value = record[____] groups.____(value, []).____(record) return ____
def group_by(records, key):
groups = {}
for record in records:
groups.setdefault(record[key], []).append(record)
return groups
There is no explicit check for the missing field, because indexing a dict already raises KeyError with the offending key in the message — which is exactly the error the contract asks for. Adding an if to raise your own KeyError would produce a worse message and more code. Reach for a hand-written check only when you can say something the built-in error cannot.
collections.defaultdict(list) removes the setdefault entirely and is a little faster. The catch is that it keeps its default-making behaviour after you return it: a caller who types groups['typo'] gets an empty list and no error, and has now silently added a key. If the result leaves your function, either return dict(groups) or use setdefault and sidestep the question.
Each group holds references to the very same dictionaries the caller passed in — grouping rearranges the pointers, it does not duplicate the data. That is what makes it cheap on a million rows, and it is also why mutating a record afterwards changes it everywhere it appears. Know which of those two you are relying on.
Do not reach for itertools.groupby expecting this behaviour. It only groups *adjacent* equal items, so on unsorted data it produces several small groups with the same key and looks broken — it is really a run-length encoder. It is correct and memory-efficient on data that is already sorted by the key, and wrong everywhere else. If the data is coming from a database, GROUP BY there instead and never build the list in memory at all.
- Accept a function instead of a field name so records can be grouped by something derived — group_by(orders, lambda o: o['date'].year) — while keeping the string form working.
- Add a `default` parameter so records missing the field go into a named bucket instead of raising, and decide which behaviour should be the default.
- Return counts instead of records when that is all the caller wants, and measure how much memory that saves on 100,000 rows.