{"id":"dsa-hash-like-a-dict","slug":"like-a-dict","title":"A map that works like dict","text":"`HashMap` works, but it doesn't read like a dict: you write `table.put(key, value)` where a dict takes `table[key] = value`. The course showed how a class decides what `<` and `==` mean for its objects (`__lt__` and `__eq__`), and how a `for` loop goes through it (`__iter__`). The rest of a dict's syntax works the same way: each piece calls a method with a special name.\n\n| You write | Python calls |\n| --- | --- |\n| `m[key]` | `m.__getitem__(key)` |\n| `m[key] = value` | `m.__setitem__(key, value)` |\n| `del m[key]` | `m.__delitem__(key)` |\n| `key in m` | `m.__contains__(key)` |\n| `len(m)` | `m.__len__()` |\n| `for key in m:` | `m.__iter__()` |\n\nHere is the map with square brackets for reading and storing, and with `len` (the growing of the last step is left out to keep it short):\n\n```python\nclass HashMap:\n    def __init__(self):\n        self.buckets = [[] for i in range(8)]\n        self.count = 0\n\n    def _index(self, key):\n        return hash(key) % len(self.buckets)\n\n    def __setitem__(self, key, value):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                pair[1] = value\n                return\n        bucket.append([key, value])\n        self.count += 1\n\n    def __getitem__(self, key):\n        for pair in self.buckets[self._index(key)]:\n            if pair[0] == key:\n                return pair[1]\n        raise KeyError(key)\n\n    def __len__(self):\n        return self.count\n\n\nstock = HashMap()\nstock[\"apple\"] = 12\nstock[\"pear\"] = 5\nstock[\"apple\"] += 3\nprint(stock[\"apple\"], len(stock))\nprint(stock[\"plum\"])\n```\n\n```output\n15 2\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 33, in <module>\n    print(stock[\"plum\"])\n          ~~~~~^^^^^^^^\n  File \"main.py\", line 22, in __getitem__\n    raise KeyError(key)\nKeyError: 'plum'\n```\n\n`stock[\"apple\"] += 3` calls both: `__getitem__` reads 12, and `__setitem__` stores 15.\n\n### A missing key is an error\n\nA missing key raises **`KeyError`**, as it does in a dict, instead of returning `None`. `None` could be a real value in the map, and a `None` that travels on through a program is harder to track down than an error at the line that asked for the key. `raise KeyError(key)` puts the key in the message. When a missing key is normal, a dict has `get(key, default)`, and a map of your own can have one too.\n\n`__len__` returns the count that `__setitem__` keeps up to date, so `len(stock)` is O(1), as it is for a dict. Adding up the lengths of all the buckets would give the same number, but in O(m), a step for every bucket.\n\n### Going through the keys\n\n`__iter__` can be a generator that yields each key, bucket after bucket:\n\n```python\ndef __iter__(self):\n    for bucket in self.buckets:\n        for pair in bucket:\n            yield pair[0]\n```\n\nThen `for`, `list`, `sorted` and `max` all work on the map. The keys come out in the order of their buckets; a dict gives them in the order they were added (more on that in the last step).\n\nOne more thing about `__iter__`: without a `__contains__`, `in` still works, by going through `__iter__` until it meets the key. That's right, and it's O(n):\n\n```python\nclass Shelf:\n    def __iter__(self):\n        for book in [\"Dune\", \"Emma\", \"Ulysses\", \"Walden\"]:\n            print(\"checking\", book)\n            yield book\n\n\nprint(\"Ulysses\" in Shelf())\n```\n\n```output\nchecking Dune\nchecking Emma\nchecking Ulysses\nTrue\n```\n\nA `__contains__` that looks only in the key's bucket is O(1) on average.\n\n### Changing a dict while you loop over it\n\nA loop over a dict walks through its table. Adding or removing keys during the loop could change the table under it (adding can even rebuild it, as the last step showed), so Python stops the loop with an error:\n\n```python\nstock = {\"apple\": 0, \"pear\": 3, \"plum\": 0}\nfor item in stock:\n    if stock[item] == 0:\n        del stock[item]\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 2, in <module>\n    for item in stock:\n                ^^^^^\nRuntimeError: dictionary changed size during iteration\n```\n\nLoop over a copy of the keys instead: `for item in list(stock):` makes the list before anything changes. Giving a key that's already there a new value is fine, since the dict keeps the same keys.\n","task":"Add `__contains__` and `__delitem__` to the map. `key in m` must look only in the key's bucket: the hidden test asks about 40,000 keys in a map of 40,000 pairs. `del m[key]` removes the key's pair, makes `self.count` one smaller, and raises `KeyError` for a key that isn't in the map. This program:\n\n```python\nm = HashMap()\nm[\"ada\"] = 36\nm[\"ben\"] = 41\nprint(\"ada\" in m, \"eve\" in m)\ndel m[\"ada\"]\nprint(\"ada\" in m, len(m))\ntry:\n    del m[\"eve\"]\nexcept KeyError:\n    print(\"no eve\")\n```\n\nprints:\n\n```\nTrue False\nFalse 1\nno eve\n```\n","kind":"code","seed":"class HashMap:\n    def __init__(self):\n        self.buckets = [[] for i in range(8)]\n        self.count = 0\n\n    def _index(self, key):\n        return hash(key) % len(self.buckets)\n\n    def _grow(self):\n        old = self.buckets\n        self.buckets = [[] for i in range(len(old) * 2)]\n        for bucket in old:\n            for pair in bucket:\n                self.buckets[self._index(pair[0])].append(pair)\n\n    def __setitem__(self, key, value):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                pair[1] = value\n                return\n        bucket.append([key, value])\n        self.count += 1\n        if self.count / len(self.buckets) > 0.75:\n            self._grow()\n\n    def __getitem__(self, key):\n        for pair in self.buckets[self._index(key)]:\n            if pair[0] == key:\n                return pair[1]\n        raise KeyError(key)\n\n    def __len__(self):\n        return self.count\n\n    def __contains__(self, key):\n        pass\n\n    def __delitem__(self, key):\n        pass\n\n\nif __name__ == \"__main__\":\n    m = HashMap()\n    m[\"ada\"] = 36\n    m[\"ben\"] = 41\n    print(\"ada\" in m, \"eve\" in m)\n    del m[\"ada\"]\n    print(\"ada\" in m, len(m))\n    try:\n        del m[\"eve\"]\n    except KeyError:\n        print(\"no eve\")\n","solution":"class HashMap:\n    def __init__(self):\n        self.buckets = [[] for i in range(8)]\n        self.count = 0\n\n    def _index(self, key):\n        return hash(key) % len(self.buckets)\n\n    def _grow(self):\n        old = self.buckets\n        self.buckets = [[] for i in range(len(old) * 2)]\n        for bucket in old:\n            for pair in bucket:\n                self.buckets[self._index(pair[0])].append(pair)\n\n    def __setitem__(self, key, value):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                pair[1] = value\n                return\n        bucket.append([key, value])\n        self.count += 1\n        if self.count / len(self.buckets) > 0.75:\n            self._grow()\n\n    def __getitem__(self, key):\n        for pair in self.buckets[self._index(key)]:\n            if pair[0] == key:\n                return pair[1]\n        raise KeyError(key)\n\n    def __len__(self):\n        return self.count\n\n    def __contains__(self, key):\n        for pair in self.buckets[self._index(key)]:\n            if pair[0] == key:\n                return True\n        return False\n\n    def __delitem__(self, key):\n        bucket = self.buckets[self._index(key)]\n        for i in range(len(bucket)):\n            if bucket[i][0] == key:\n                del bucket[i]\n                self.count -= 1\n                return\n        raise KeyError(key)\n\n\nif __name__ == \"__main__\":\n    m = HashMap()\n    m[\"ada\"] = 36\n    m[\"ben\"] = 41\n    print(\"ada\" in m, \"eve\" in m)\n    del m[\"ada\"]\n    print(\"ada\" in m, len(m))\n    try:\n        del m[\"eve\"]\n    except KeyError:\n        print(\"no eve\")\n","hints":["`__contains__` works like `__getitem__`: go through the pairs of the key's bucket only, and return `True` when one of them has the key. After the loop, return `False`.","`__delitem__`: find the position of the pair in its bucket with `for i in range(len(bucket))`. When it matches, `del bucket[i]`, make `self.count` one smaller and return.","When the loop ends without finding the key, `raise KeyError(key)`. A `__contains__` that looks through every bucket gives the right answers too, but in O(n): too slow for the big test."],"tests":[{"name":"Test 1","stdin":"","call":"m = HashMap()\nm[\"ada\"] = 36\nm[\"ben\"] = 41\nprint(\"ada\" in m, \"eve\" in m)\ndel m[\"ada\"]\nprint(\"ada\" in m, len(m))\ntry:\n    del m[\"eve\"]\nexcept KeyError:\n    print(\"no eve\")\n","expect":"True False\nFalse 1\nno eve","hidden":false},{"name":"Test 2","stdin":"","call":"m = HashMap()\nm[1] = \"one\"\nm[9] = \"nine\"\nm[17] = \"seventeen\"\ndel m[9]\nprint(1 in m, 9 in m, 17 in m, m[17], len(m))\n","expect":"True False True seventeen 2","hidden":true},{"name":"Test 3","stdin":"","call":"m = HashMap()\nm[\"x\"] = None\nprint(\"x\" in m, \"y\" in m)\ndel m[\"x\"]\nm[\"x\"] = 5\nprint(m[\"x\"], len(m))\n","expect":"True False\n5 1","hidden":true},{"name":"Test 4","stdin":"","call":"m = HashMap()\ntry:\n    del m[3]\nexcept KeyError:\n    print(\"KeyError\")\nprint(len(m), 3 in m)\n","expect":"KeyError\n0 False","hidden":true},{"name":"Test 5","stdin":"","call":"m = HashMap()\nfor i in range(20):\n    m[i] = i\nfor i in range(0, 20, 2):\n    del m[i]\nprint(len(m), [k for k in range(20) if k in m])\n","expect":"10 [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]","hidden":true},{"name":"Test 6","stdin":"","call":"m = HashMap()\nfor i in range(40_000):\n    m[i * 3] = i\nprint(sum(1 for i in range(40_000) if i * 2 in m), len(m))\n","expect":"13334 40000","hidden":true,"big":true}],"require":[],"forbid":[{"pattern":"[{}]|\\bset\\s*\\(|\\bdict\\s*\\(","message":"Work with your own buckets, without Python's `set` or `dict`."}],"more":[{"task":"Add two generators to the map: `__iter__`, which yields every key, and `items()`, which yields every pair as a tuple `(key, value)`. With `__iter__`, the map works with `for`, `list`, `sorted` and `max` like a dict. The order of the keys doesn't matter: the tests sort them.\n\nFor example, this program:\n\n```python\nm = HashMap()\nm[\"fig\"] = 3\nm[\"apple\"] = 7\nm[\"kiwi\"] = 5\nprint(sorted(m))\nprint(sorted(m.items()))\nprint(max(m, key=lambda k: m[k]))\n```\n\nprints:\n\n```\n['apple', 'fig', 'kiwi']\n[('apple', 7), ('fig', 3), ('kiwi', 5)]\napple\n```\n","kind":"code","seed":"class HashMap:\n    def __init__(self):\n        self.buckets = [[] for i in range(8)]\n        self.count = 0\n\n    def _index(self, key):\n        return hash(key) % len(self.buckets)\n\n    def _grow(self):\n        old = self.buckets\n        self.buckets = [[] for i in range(len(old) * 2)]\n        for bucket in old:\n            for pair in bucket:\n                self.buckets[self._index(pair[0])].append(pair)\n\n    def __setitem__(self, key, value):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                pair[1] = value\n                return\n        bucket.append([key, value])\n        self.count += 1\n        if self.count / len(self.buckets) > 0.75:\n            self._grow()\n\n    def __getitem__(self, key):\n        for pair in self.buckets[self._index(key)]:\n            if pair[0] == key:\n                return pair[1]\n        raise KeyError(key)\n\n    def __len__(self):\n        return self.count\n\n    def __iter__(self):\n        pass\n\n    def items(self):\n        pass\n\n\nif __name__ == \"__main__\":\n    m = HashMap()\n    m[\"fig\"] = 3\n    m[\"apple\"] = 7\n    m[\"kiwi\"] = 5\n    print(sorted(m))\n    print(sorted(m.items()))\n    print(max(m, key=lambda k: m[k]))\n","solution":"class HashMap:\n    def __init__(self):\n        self.buckets = [[] for i in range(8)]\n        self.count = 0\n\n    def _index(self, key):\n        return hash(key) % len(self.buckets)\n\n    def _grow(self):\n        old = self.buckets\n        self.buckets = [[] for i in range(len(old) * 2)]\n        for bucket in old:\n            for pair in bucket:\n                self.buckets[self._index(pair[0])].append(pair)\n\n    def __setitem__(self, key, value):\n        bucket = self.buckets[self._index(key)]\n        for pair in bucket:\n            if pair[0] == key:\n                pair[1] = value\n                return\n        bucket.append([key, value])\n        self.count += 1\n        if self.count / len(self.buckets) > 0.75:\n            self._grow()\n\n    def __getitem__(self, key):\n        for pair in self.buckets[self._index(key)]:\n            if pair[0] == key:\n                return pair[1]\n        raise KeyError(key)\n\n    def __len__(self):\n        return self.count\n\n    def __iter__(self):\n        for bucket in self.buckets:\n            for pair in bucket:\n                yield pair[0]\n\n    def items(self):\n        for bucket in self.buckets:\n            for pair in bucket:\n                yield (pair[0], pair[1])\n\n\nif __name__ == \"__main__\":\n    m = HashMap()\n    m[\"fig\"] = 3\n    m[\"apple\"] = 7\n    m[\"kiwi\"] = 5\n    print(sorted(m))\n    print(sorted(m.items()))\n    print(max(m, key=lambda k: m[k]))\n","hints":["A method with `yield` in it is a generator: each `yield` gives the loop one more item. Go through every bucket, and every pair in each bucket.","`__iter__` yields `pair[0]`, and `items` yields `(pair[0], pair[1])`.","Yield a new tuple in `items`, not the pair list itself: a caller who changed that list would change the map."],"tests":[{"name":"Test 1","stdin":"","call":"m = HashMap()\nm[\"fig\"] = 3\nm[\"apple\"] = 7\nm[\"kiwi\"] = 5\nprint(sorted(m))\nprint(sorted(m.items()))\nprint(max(m, key=lambda k: m[k]))\n","expect":"['apple', 'fig', 'kiwi']\n[('apple', 7), ('fig', 3), ('kiwi', 5)]\napple","hidden":false},{"name":"Test 2","stdin":"","call":"m = HashMap()\nprint(list(m), list(m.items()))\n","expect":"[] []","hidden":true},{"name":"Test 3","stdin":"","call":"m = HashMap()\nfor i in range(20):\n    m[i] = i * i\nprint(sorted(m) == list(range(20)), sum(value for key, value in m.items()))\n","expect":"True 2470","hidden":true},{"name":"Test 4","stdin":"","call":"m = HashMap()\nm[2] = \"b\"\nm[1] = \"a\"\ntotal = \"\"\nfor key in sorted(m):\n    total += m[key]\nprint(total, sorted(m), len(list(m)))\n","expect":"ab [1, 2] 2","hidden":true},{"name":"Test 5","stdin":"","call":"m = HashMap()\nm[\"tea\"] = 4\nfor key, value in m.items():\n    print(type(key).__name__, type(value).__name__, (key, value) == (\"tea\", 4))\n","expect":"str int True","hidden":true}],"require":[],"forbid":[{"pattern":"[{}]|\\bset\\s*\\(|\\bdict\\s*\\(","message":"Work with your own buckets, without Python's `set` or `dict`."}]},{"task":"`remove_sold_out(stock)` should delete every item whose count is 0 from the dict it gets, so that the caller's dict changes. It stops with `RuntimeError: dictionary changed size during iteration`. Fix it.\n\nFor example, this program:\n\n```python\nstock = {\"apple\": 0, \"pear\": 3, \"plum\": 0, \"fig\": 8}\nremove_sold_out(stock)\nprint(stock)\n```\n\nshould print:\n\n```\n{'pear': 3, 'fig': 8}\n```\n","kind":"code","seed":"def remove_sold_out(stock):\n    for item in stock:\n        if stock[item] == 0:\n            del stock[item]\n\n\nif __name__ == \"__main__\":\n    stock = {\"apple\": 0, \"pear\": 3, \"plum\": 0, \"fig\": 8}\n    remove_sold_out(stock)\n    print(stock)\n","solution":"def remove_sold_out(stock):\n    for item in list(stock):\n        if stock[item] == 0:\n            del stock[item]\n\n\nif __name__ == \"__main__\":\n    stock = {\"apple\": 0, \"pear\": 3, \"plum\": 0, \"fig\": 8}\n    remove_sold_out(stock)\n    print(stock)\n","hints":["The loop goes through the dict while `del` changes it. Loop over a list of the keys made before the loop starts instead.","`for item in list(stock):` copies the keys first, so deleting from `stock` inside the loop is safe.","A new dict made with a comprehension, `stock = {...}`, would only give the local name `stock` a new dict: the caller's dict wouldn't change."],"tests":[{"name":"Test 1","stdin":"","call":"stock = {\"apple\": 0, \"pear\": 3, \"plum\": 0, \"fig\": 8}\nremove_sold_out(stock)\nprint(stock)\n","expect":"{'pear': 3, 'fig': 8}","hidden":false},{"name":"Test 2","stdin":"","call":"stock = {\"tea\": 2, \"jam\": 5}\nremove_sold_out(stock)\nprint(stock)\n","expect":"{'tea': 2, 'jam': 5}","hidden":true},{"name":"Test 3","stdin":"","call":"stock = {\"egg\": 0, \"oat\": 0}\nremove_sold_out(stock)\nprint(stock)\n","expect":"{}","hidden":true},{"name":"Test 4","stdin":"","call":"stock = {}\nremove_sold_out(stock)\nprint(stock)\n","expect":"{}","hidden":true},{"name":"Test 5","stdin":"","call":"stock = {\"rice\": 4, \"salt\": 0}\nsame = stock\nremove_sold_out(stock)\nprint(same)\n","expect":"{'rice': 4}","hidden":true}],"require":[],"forbid":[]}]}