From 69054ba73c27a2e47b85d48e05ccb615bb35681f Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Fri, 29 Oct 2021 07:58:09 -0700 Subject: [PATCH 01/14] Adds repr and doctest of current behavior linkedlist in other/lru_cache --- other/lru_cache.py | 81 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/other/lru_cache.py b/other/lru_cache.py index b74c0a45caf9..9dd3910a92ec 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -6,6 +6,10 @@ class DoubleLinkedListNode: """ Double Linked List Node built specifically for LRU Cache + + >>> node = DoubleLinkedListNode(1,1) + >>> node + Node: key: 1, val: 1, has next: False, has prev: False """ def __init__(self, key: int, val: int): @@ -14,10 +18,66 @@ def __init__(self, key: int, val: int): self.next = None self.prev = None + def __repr__(self) -> str: + return "Node: key: {}, val: {}, has next: {}, has prev: {}".format( + self.key, self.val, self.next is not None, self.prev is not None + ) + class DoubleLinkedList: """ Double Linked List built specifically for LRU Cache + + >>> dll: DoubleLinkedList = DoubleLinkedList() + >>> dll + DoubleLinkedList:, + Node: key: None, val: None, has next: True, has prev: False, + Node: key: None, val: None, has next: False, has prev: True + + >>> new_node = DoubleLinkedListNode(1,1) + >>> new_node + Node: key: 1, val: 1, has next: False, has prev: False + + >>> dll.add(new_node) + >>> dll + DoubleLinkedList:, + Node: key: None, val: None, has next: True, has prev: False, + Node: key: 1, val: 1, has next: True, has prev: True, + Node: key: None, val: None, has next: False, has prev: True + + >>> # node is mutated + >>> new_node + Node: key: 1, val: 1, has next: True, has prev: True + + >>> removed_node = dll.remove(new_node) + >>> dll + DoubleLinkedList:, + Node: key: None, val: None, has next: True, has prev: False, + Node: key: None, val: None, has next: False, has prev: True + + >>> removed_node = dll.remove(first_node) + >>> assert removed_node == first_node + >>> dll + DoubleLinkedList, + Node: key: None, val: None, has next: True, has prev: False, + Node: key: 2, val: 20, has next: True, has prev: True, + Node: key: None, val: None, has next: False, has prev: True + + + >>> # Attempt to remove node not on list + >>> removed_node = dll.remove(first_node) + >>> removed_node is None + True + + >>> # Attempt to remove head or rear + >>> dll.head + Node: key: None, val: None, has next: True, has prev: False + >>> dll.remove(dll.head) is None + True + + >>> # Attempt to remove head or rear + >>> # removed_node = dll.remove(DoubleLinkedListNode(None, None)) + """ def __init__(self): @@ -25,6 +85,15 @@ def __init__(self): self.rear = DoubleLinkedListNode(None, None) self.head.next, self.rear.prev = self.rear, self.head + def __repr__(self) -> str: + rep = ["DoubleLinkedList:"] + node = self.head + while node.next is not None: + rep.append(str(node)) + node = node.next + rep.append(str(self.rear)) + return ",\n ".join(rep) + def add(self, node: DoubleLinkedListNode) -> None: """ Adds the given node to the end of the list (before rear) @@ -54,19 +123,19 @@ class LRUCache: >>> cache = LRUCache(2) >>> cache.set(1, 1) - >>> cache.set(2, 2) - >>> cache.get(1) 1 >>> cache.set(3, 3) - >>> cache.get(2) # None returned + >>> cache.get(2) is None + True >>> cache.set(4, 4) - >>> cache.get(1) # None returned + >>> cache.get(1) is None + True >>> cache.get(3) 3 @@ -129,8 +198,8 @@ def __contains__(self, key: int) -> bool: def get(self, key: int) -> int | None: """ - Returns the value for the input key and updates the Double Linked List. Returns - None if key is not present in cache + Returns the value for the input key and updates the Double Linked List. + Returns None if key is not present in cache """ if key in self.cache: From db3a7b62412dcbdc5cb8131d80dd6f2de84840eb Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Fri, 29 Oct 2021 08:17:59 -0700 Subject: [PATCH 02/14] Blocks removal of head or tail of double linked list --- other/lru_cache.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/other/lru_cache.py b/other/lru_cache.py index 9dd3910a92ec..c23f39082928 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -76,7 +76,11 @@ class DoubleLinkedList: True >>> # Attempt to remove head or rear - >>> # removed_node = dll.remove(DoubleLinkedListNode(None, None)) + >>> dll.rear + Node: key: None, val: None, has next: False, has prev: True + >>> dll.remove(dll.rear) is None + True + """ @@ -103,14 +107,22 @@ def add(self, node: DoubleLinkedListNode) -> None: temp.next, node.prev = node, temp self.rear.prev, node.next = node, self.rear - def remove(self, node: DoubleLinkedListNode) -> DoubleLinkedListNode: + def remove(self, node: DoubleLinkedListNode) -> DoubleLinkedListNode | None: """ Removes and returns the given node from the list + + Returns None if node.prev or node.next is None """ - temp_last, temp_next = node.prev, node.next - node.prev, node.next = None, None - temp_last.next, temp_next.prev = temp_next, temp_last + if node.prev is None: + return None + elif node.next is None: + return None + else: + node.prev.next = node.next + node.next.prev = node.prev + node.prev = None + node.next = None return node From 91effc8611a16790385589e4a39ac1f04d32390c Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Fri, 29 Oct 2021 08:43:09 -0700 Subject: [PATCH 03/14] clarifies add() logic for double linked list in other/lru_cache --- other/lru_cache.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/other/lru_cache.py b/other/lru_cache.py index c23f39082928..50dc1384bf23 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -103,9 +103,11 @@ def add(self, node: DoubleLinkedListNode) -> None: Adds the given node to the end of the list (before rear) """ - temp = self.rear.prev - temp.next, node.prev = node, temp - self.rear.prev, node.next = node, self.rear + previous = self.rear.prev + previous.next = node + node.prev = previous + self.rear.prev = node + node.next = self.rear def remove(self, node: DoubleLinkedListNode) -> DoubleLinkedListNode | None: """ From 8e15ab0389934c0ff80ee592d3105c9ea1b682d5 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Fri, 29 Oct 2021 12:47:12 -0700 Subject: [PATCH 04/14] expands doctests to compare cache and lru cache --- other/lru_cache.py | 51 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/other/lru_cache.py b/other/lru_cache.py index 50dc1384bf23..8c64a0ea6a4d 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -30,29 +30,36 @@ class DoubleLinkedList: >>> dll: DoubleLinkedList = DoubleLinkedList() >>> dll - DoubleLinkedList:, + DoubleLinkedList, Node: key: None, val: None, has next: True, has prev: False, Node: key: None, val: None, has next: False, has prev: True - >>> new_node = DoubleLinkedListNode(1,1) - >>> new_node - Node: key: 1, val: 1, has next: False, has prev: False + >>> first_node = DoubleLinkedListNode(1,10) + >>> first_node + Node: key: 1, val: 10, has next: False, has prev: False + - >>> dll.add(new_node) + >>> dll.add(first_node) >>> dll - DoubleLinkedList:, + DoubleLinkedList, Node: key: None, val: None, has next: True, has prev: False, - Node: key: 1, val: 1, has next: True, has prev: True, + Node: key: 1, val: 10, has next: True, has prev: True, Node: key: None, val: None, has next: False, has prev: True >>> # node is mutated - >>> new_node - Node: key: 1, val: 1, has next: True, has prev: True + >>> first_node + Node: key: 1, val: 10, has next: True, has prev: True - >>> removed_node = dll.remove(new_node) + >>> second_node = DoubleLinkedListNode(2,20) + >>> second_node + Node: key: 2, val: 20, has next: False, has prev: False + + >>> dll.add(second_node) >>> dll - DoubleLinkedList:, + DoubleLinkedList, Node: key: None, val: None, has next: True, has prev: False, + Node: key: 1, val: 10, has next: True, has prev: True, + Node: key: 2, val: 20, has next: True, has prev: True, Node: key: None, val: None, has next: False, has prev: True >>> removed_node = dll.remove(first_node) @@ -90,7 +97,7 @@ def __init__(self): self.head.next, self.rear.prev = self.rear, self.head def __repr__(self) -> str: - rep = ["DoubleLinkedList:"] + rep = ["DoubleLinkedList"] node = self.head while node.next is not None: rep.append(str(node)) @@ -141,8 +148,28 @@ class LRUCache: >>> cache.get(1) 1 + >>> cache.list + DoubleLinkedList, + Node: key: None, val: None, has next: True, has prev: False, + Node: key: 2, val: 2, has next: True, has prev: True, + Node: key: 1, val: 1, has next: True, has prev: True, + Node: key: None, val: None, has next: False, has prev: True + + >>> cache.cache + {1: Node: key: 1, val: 1, has next: True, has prev: True, 2: Node: key: 2, val: 2, has next: True, has prev: True} + >>> cache.set(3, 3) + >>> cache.list + DoubleLinkedList, + Node: key: None, val: None, has next: True, has prev: False, + Node: key: 1, val: 1, has next: True, has prev: True, + Node: key: 3, val: 3, has next: True, has prev: True, + Node: key: None, val: None, has next: False, has prev: True + + >>> cache.cache + {1: Node: key: 1, val: 1, has next: True, has prev: True, 3: Node: key: 3, val: 3, has next: True, has prev: True} + >>> cache.get(2) is None True From 2e177f4f7b6626d1a34b19c374116ae2c4ae305d Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Tue, 2 Nov 2021 10:57:30 -0700 Subject: [PATCH 05/14] [mypy] annotates vars for other/lru_cache --- other/lru_cache.py | 48 +++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/other/lru_cache.py b/other/lru_cache.py index 8c64a0ea6a4d..97159fd58a7c 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -12,11 +12,11 @@ class DoubleLinkedListNode: Node: key: 1, val: 1, has next: False, has prev: False """ - def __init__(self, key: int, val: int): + def __init__(self, key: int | None, val: int | None): self.key = key self.val = val - self.next = None - self.prev = None + self.next: DoubleLinkedListNode | None = None + self.prev: DoubleLinkedListNode | None = None def __repr__(self) -> str: return "Node: key: {}, val: {}, has next: {}, has prev: {}".format( @@ -91,7 +91,7 @@ class DoubleLinkedList: """ - def __init__(self): + def __init__(self) -> None: self.head = DoubleLinkedListNode(None, None) self.rear = DoubleLinkedListNode(None, None) self.head.next, self.rear.prev = self.rear, self.head @@ -111,6 +111,10 @@ def add(self, node: DoubleLinkedListNode) -> None: """ previous = self.rear.prev + + # All nodes other than self.head are guaranteed to have non-None previous + assert previous is not None + previous.next = node node.prev = previous self.rear.prev = node @@ -136,6 +140,7 @@ def remove(self, node: DoubleLinkedListNode) -> DoubleLinkedListNode | None: return node +# class LRUCache(Generic[T]): class LRUCache: """ LRU Cache to store a given capacity of data. Can be used as a stand-alone object @@ -201,7 +206,7 @@ class LRUCache: """ # class variable to map the decorator functions to their respective instance - decorator_function_to_instance_map = {} + decorator_function_to_instance_map: dict[Callable, LRUCache] = {} def __init__(self, capacity: int): self.list = DoubleLinkedList() @@ -209,7 +214,9 @@ def __init__(self, capacity: int): self.num_keys = 0 self.hits = 0 self.miss = 0 - self.cache = {} + # self.cache: dict[int, int] = {} + # self.cache: dict[int, T] = {} + self.cache: dict[int, DoubleLinkedListNode] = {} def __repr__(self) -> str: """ @@ -245,8 +252,14 @@ def get(self, key: int) -> int | None: if key in self.cache: self.hits += 1 - self.list.add(self.list.remove(self.cache[key])) - return self.cache[key].val + value_node = self.cache[key] + node = self.list.remove(self.cache[key]) + assert node == value_node + + # node is guaranteed not None because it is in self.cache + assert node is not None + self.list.add(node) + return node.val self.miss += 1 return None @@ -257,16 +270,25 @@ def set(self, key: int, value: int) -> None: if key not in self.cache: if self.num_keys >= self.capacity: - key_to_delete = self.list.head.next.key - self.list.remove(self.cache[key_to_delete]) - del self.cache[key_to_delete] + # delete first node (oldest) when over capacity + first_node = self.list.head.next + + # guaranteed to have a non-None first node when num_keys > 0 + # explain to type checker via assertions + assert first_node is not None + assert first_node.key is not None + assert self.list.remove(first_node) is not None # node guaranteed to be in list assert node.key is not None + + del self.cache[first_node.key] self.num_keys -= 1 self.cache[key] = DoubleLinkedListNode(key, value) self.list.add(self.cache[key]) self.num_keys += 1 else: + # bump node to the end of the list, update value node = self.list.remove(self.cache[key]) + assert node is not None # node guaranteed to be in list node.val = value self.list.add(node) @@ -289,10 +311,10 @@ def cache_decorator_wrapper(*args, **kwargs): ) return result - def cache_info(): + def cache_info() -> LRUCache: return LRUCache.decorator_function_to_instance_map[func] - cache_decorator_wrapper.cache_info = cache_info + setattr(cache_decorator_wrapper, "cache_info", cache_info) return cache_decorator_wrapper From 186606026355fd4bf92980dd30b0a4f3dccd3b2f Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Tue, 2 Nov 2021 13:41:08 -0700 Subject: [PATCH 06/14] [mypy] Annotates lru_cache decorator for other/lru_cache * Higher order functions require a verbose Callable annotation --- other/lru_cache.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/other/lru_cache.py b/other/lru_cache.py index 97159fd58a7c..9788803a9325 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -293,19 +293,20 @@ def set(self, key: int, value: int) -> None: self.list.add(node) @staticmethod - def decorator(size: int = 128): + def decorator(size: int = 128) -> Callable[[Callable[[int], int]], Callable[..., int]]: """ Decorator version of LRU Cache - """ - def cache_decorator_inner(func: Callable): - def cache_decorator_wrapper(*args, **kwargs): + Decorated function must be function of int -> int + """ + def cache_decorator_inner(func: Callable[[int], int]) -> Callable[..., int]: + def cache_decorator_wrapper(*args: int) -> int: if func not in LRUCache.decorator_function_to_instance_map: LRUCache.decorator_function_to_instance_map[func] = LRUCache(size) result = LRUCache.decorator_function_to_instance_map[func].get(args[0]) if result is None: - result = func(*args, **kwargs) + result = func(*args) LRUCache.decorator_function_to_instance_map[func].set( args[0], result ) From 90cfe2d07316a995c4525df788e2df57a31fcf7a Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Tue, 2 Nov 2021 13:58:39 -0700 Subject: [PATCH 07/14] [mypy] Makes LRU_Cache generic over key and value types for other/lru_cache + no reason to force int -> int --- other/lru_cache.py | 57 +++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/other/lru_cache.py b/other/lru_cache.py index 9788803a9325..d3247dd0f003 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -1,9 +1,12 @@ from __future__ import annotations -from typing import Callable +from typing import Callable, Generic, TypeVar +T = TypeVar("T") +U = TypeVar("U") -class DoubleLinkedListNode: + +class DoubleLinkedListNode(Generic[T, U]): """ Double Linked List Node built specifically for LRU Cache @@ -12,11 +15,11 @@ class DoubleLinkedListNode: Node: key: 1, val: 1, has next: False, has prev: False """ - def __init__(self, key: int | None, val: int | None): + def __init__(self, key: T | None, val: U | None): self.key = key self.val = val - self.next: DoubleLinkedListNode | None = None - self.prev: DoubleLinkedListNode | None = None + self.next: DoubleLinkedListNode[T, U] | None = None + self.prev: DoubleLinkedListNode[T, U] | None = None def __repr__(self) -> str: return "Node: key: {}, val: {}, has next: {}, has prev: {}".format( @@ -24,7 +27,7 @@ def __repr__(self) -> str: ) -class DoubleLinkedList: +class DoubleLinkedList(Generic[T, U]): """ Double Linked List built specifically for LRU Cache @@ -92,8 +95,8 @@ class DoubleLinkedList: """ def __init__(self) -> None: - self.head = DoubleLinkedListNode(None, None) - self.rear = DoubleLinkedListNode(None, None) + self.head: DoubleLinkedListNode[T, U] = DoubleLinkedListNode(None, None) + self.rear: DoubleLinkedListNode[T, U] = DoubleLinkedListNode(None, None) self.head.next, self.rear.prev = self.rear, self.head def __repr__(self) -> str: @@ -105,7 +108,7 @@ def __repr__(self) -> str: rep.append(str(self.rear)) return ",\n ".join(rep) - def add(self, node: DoubleLinkedListNode) -> None: + def add(self, node: DoubleLinkedListNode[T, U]) -> None: """ Adds the given node to the end of the list (before rear) """ @@ -120,7 +123,9 @@ def add(self, node: DoubleLinkedListNode) -> None: self.rear.prev = node node.next = self.rear - def remove(self, node: DoubleLinkedListNode) -> DoubleLinkedListNode | None: + def remove( + self, node: DoubleLinkedListNode[T, U] + ) -> DoubleLinkedListNode[T, U] | None: """ Removes and returns the given node from the list @@ -140,8 +145,7 @@ def remove(self, node: DoubleLinkedListNode) -> DoubleLinkedListNode | None: return node -# class LRUCache(Generic[T]): -class LRUCache: +class LRUCache(Generic[T, U]): """ LRU Cache to store a given capacity of data. Can be used as a stand-alone object or as a function decorator. @@ -206,17 +210,15 @@ class LRUCache: """ # class variable to map the decorator functions to their respective instance - decorator_function_to_instance_map: dict[Callable, LRUCache] = {} + decorator_function_to_instance_map: dict[Callable[[T], U], LRUCache[T, U]] = {} def __init__(self, capacity: int): - self.list = DoubleLinkedList() + self.list: DoubleLinkedList[T, U] = DoubleLinkedList() self.capacity = capacity self.num_keys = 0 self.hits = 0 self.miss = 0 - # self.cache: dict[int, int] = {} - # self.cache: dict[int, T] = {} - self.cache: dict[int, DoubleLinkedListNode] = {} + self.cache: dict[T, DoubleLinkedListNode[T, U]] = {} def __repr__(self) -> str: """ @@ -229,7 +231,7 @@ def __repr__(self) -> str: f"capacity={self.capacity}, current size={self.num_keys})" ) - def __contains__(self, key: int) -> bool: + def __contains__(self, key: T) -> bool: """ >>> cache = LRUCache(1) @@ -244,7 +246,7 @@ def __contains__(self, key: int) -> bool: return key in self.cache - def get(self, key: int) -> int | None: + def get(self, key: T) -> U | None: """ Returns the value for the input key and updates the Double Linked List. Returns None if key is not present in cache @@ -252,7 +254,7 @@ def get(self, key: int) -> int | None: if key in self.cache: self.hits += 1 - value_node = self.cache[key] + value_node: DoubleLinkedListNode[T, U] = self.cache[key] node = self.list.remove(self.cache[key]) assert node == value_node @@ -263,7 +265,7 @@ def get(self, key: int) -> int | None: self.miss += 1 return None - def set(self, key: int, value: int) -> None: + def set(self, key: T, value: U) -> None: """ Sets the value for the input key and updates the Double Linked List """ @@ -277,7 +279,9 @@ def set(self, key: int, value: int) -> None: # explain to type checker via assertions assert first_node is not None assert first_node.key is not None - assert self.list.remove(first_node) is not None # node guaranteed to be in list assert node.key is not None + assert ( + self.list.remove(first_node) is not None + ) # node guaranteed to be in list assert node.key is not None del self.cache[first_node.key] self.num_keys -= 1 @@ -293,14 +297,15 @@ def set(self, key: int, value: int) -> None: self.list.add(node) @staticmethod - def decorator(size: int = 128) -> Callable[[Callable[[int], int]], Callable[..., int]]: + def decorator(size: int = 128) -> Callable[[Callable[[T], U]], Callable[..., U]]: """ Decorator version of LRU Cache - Decorated function must be function of int -> int + Decorated function must be function of T -> U """ - def cache_decorator_inner(func: Callable[[int], int]) -> Callable[..., int]: - def cache_decorator_wrapper(*args: int) -> int: + + def cache_decorator_inner(func: Callable[[T], U]) -> Callable[..., U]: + def cache_decorator_wrapper(*args: T) -> U: if func not in LRUCache.decorator_function_to_instance_map: LRUCache.decorator_function_to_instance_map[func] = LRUCache(size) From a5b7b6292a691b94eb19fc2d18e51f869c259f7d Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Tue, 2 Nov 2021 14:45:20 -0700 Subject: [PATCH 08/14] [mypy] makes decorator a classmethod for access to class generic types --- other/lru_cache.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/other/lru_cache.py b/other/lru_cache.py index d3247dd0f003..a71d76628b74 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -296,8 +296,10 @@ def set(self, key: T, value: U) -> None: node.val = value self.list.add(node) - @staticmethod - def decorator(size: int = 128) -> Callable[[Callable[[T], U]], Callable[..., U]]: + @classmethod + def decorator( + cls, size: int = 128 + ) -> Callable[[Callable[[T], U]], Callable[..., U]]: """ Decorator version of LRU Cache @@ -306,19 +308,17 @@ def decorator(size: int = 128) -> Callable[[Callable[[T], U]], Callable[..., U]] def cache_decorator_inner(func: Callable[[T], U]) -> Callable[..., U]: def cache_decorator_wrapper(*args: T) -> U: - if func not in LRUCache.decorator_function_to_instance_map: - LRUCache.decorator_function_to_instance_map[func] = LRUCache(size) + if func not in cls.decorator_function_to_instance_map: + cls.decorator_function_to_instance_map[func] = LRUCache(size) - result = LRUCache.decorator_function_to_instance_map[func].get(args[0]) + result = cls.decorator_function_to_instance_map[func].get(args[0]) if result is None: result = func(*args) - LRUCache.decorator_function_to_instance_map[func].set( - args[0], result - ) + cls.decorator_function_to_instance_map[func].set(args[0], result) return result - def cache_info() -> LRUCache: - return LRUCache.decorator_function_to_instance_map[func] + def cache_info() -> LRUCache[T, U]: + return cls.decorator_function_to_instance_map[func] setattr(cache_decorator_wrapper, "cache_info", cache_info) From ef4401f1d11b8e1b3fe85348a8ba00277fe61fb8 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Wed, 3 Nov 2021 03:31:48 -0700 Subject: [PATCH 09/14] breaks two long lines in doctest for other/lru_cache --- other/lru_cache.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/other/lru_cache.py b/other/lru_cache.py index a71d76628b74..a6884a043149 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -165,7 +165,8 @@ class LRUCache(Generic[T, U]): Node: key: None, val: None, has next: False, has prev: True >>> cache.cache - {1: Node: key: 1, val: 1, has next: True, has prev: True, 2: Node: key: 2, val: 2, has next: True, has prev: True} + {1: Node: key: 1, val: 1, has next: True, has prev: True, \ +2: Node: key: 2, val: 2, has next: True, has prev: True} >>> cache.set(3, 3) @@ -177,7 +178,8 @@ class LRUCache(Generic[T, U]): Node: key: None, val: None, has next: False, has prev: True >>> cache.cache - {1: Node: key: 1, val: 1, has next: True, has prev: True, 3: Node: key: 3, val: 3, has next: True, has prev: True} + {1: Node: key: 1, val: 1, has next: True, has prev: True, \ +3: Node: key: 3, val: 3, has next: True, has prev: True} >>> cache.get(2) is None True From 895637f7030b1050f4a840b41fd133d33fc7ae48 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Wed, 3 Nov 2021 04:06:37 -0700 Subject: [PATCH 10/14] simplifies boundary test remove() for other/lru_cache --- other/lru_cache.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/other/lru_cache.py b/other/lru_cache.py index a6884a043149..1ebf4308dbec 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -132,16 +132,13 @@ def remove( Returns None if node.prev or node.next is None """ - if node.prev is None: + if node.prev is None or node.next is None: return None - elif node.next is None: - return None - else: - node.prev.next = node.next - node.next.prev = node.prev - node.prev = None - node.next = None + node.prev.next = node.next + node.next.prev = node.prev + node.prev = None + node.next = None return node From 8b6fa2e50a3de8202d6cdafdd6ece3384dbb06d2 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Wed, 3 Nov 2021 04:30:26 -0700 Subject: [PATCH 11/14] [mypy] Annotates, adds doctests, and makes Generic other/lfu_cache See also commits to other/lru_cache which guided these --- other/lfu_cache.py | 233 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 180 insertions(+), 53 deletions(-) diff --git a/other/lfu_cache.py b/other/lfu_cache.py index 88167ac1f2cb..200f388af11a 100644 --- a/other/lfu_cache.py +++ b/other/lfu_cache.py @@ -1,61 +1,165 @@ from __future__ import annotations -from typing import Callable +from typing import Callable, Generic, TypeVar +T = TypeVar("T") +U = TypeVar("U") -class DoubleLinkedListNode: + +class DoubleLinkedListNode(Generic[T, U]): """ Double Linked List Node built specifically for LFU Cache + + >>> node = DoubleLinkedListNode(1,1) + >>> node + Node: key: 1, val: 1, freq: 0, has next: False, has prev: False """ - def __init__(self, key: int, val: int): + def __init__(self, key: T | None, val: U | None): self.key = key self.val = val - self.freq = 0 - self.next = None - self.prev = None + self.freq: int = 0 + self.next: DoubleLinkedListNode[T, U] | None = None + self.prev: DoubleLinkedListNode[T, U] | None = None + + def __repr__(self) -> str: + return "Node: key: {}, val: {}, freq: {}, has next: {}, has prev: {}".format( + self.key, self.val, self.freq, self.next is not None, self.prev is not None + ) -class DoubleLinkedList: +class DoubleLinkedList(Generic[T, U]): """ Double Linked List built specifically for LFU Cache + + >>> dll: DoubleLinkedList = DoubleLinkedList() + >>> dll + DoubleLinkedList, + Node: key: None, val: None, freq: 0, has next: True, has prev: False, + Node: key: None, val: None, freq: 0, has next: False, has prev: True + + >>> first_node = DoubleLinkedListNode(1,10) + >>> first_node + Node: key: 1, val: 10, freq: 0, has next: False, has prev: False + + + >>> dll.add(first_node) + >>> dll + DoubleLinkedList, + Node: key: None, val: None, freq: 0, has next: True, has prev: False, + Node: key: 1, val: 10, freq: 1, has next: True, has prev: True, + Node: key: None, val: None, freq: 0, has next: False, has prev: True + + >>> # node is mutated + >>> first_node + Node: key: 1, val: 10, freq: 1, has next: True, has prev: True + + >>> second_node = DoubleLinkedListNode(2,20) + >>> second_node + Node: key: 2, val: 20, freq: 0, has next: False, has prev: False + + >>> dll.add(second_node) + >>> dll + DoubleLinkedList, + Node: key: None, val: None, freq: 0, has next: True, has prev: False, + Node: key: 1, val: 10, freq: 1, has next: True, has prev: True, + Node: key: 2, val: 20, freq: 1, has next: True, has prev: True, + Node: key: None, val: None, freq: 0, has next: False, has prev: True + + >>> removed_node = dll.remove(first_node) + >>> assert removed_node == first_node + >>> dll + DoubleLinkedList, + Node: key: None, val: None, freq: 0, has next: True, has prev: False, + Node: key: 2, val: 20, freq: 1, has next: True, has prev: True, + Node: key: None, val: None, freq: 0, has next: False, has prev: True + + + >>> # Attempt to remove node not on list + >>> removed_node = dll.remove(first_node) + >>> removed_node is None + True + + >>> # Attempt to remove head or rear + >>> dll.head + Node: key: None, val: None, freq: 0, has next: True, has prev: False + >>> dll.remove(dll.head) is None + True + + >>> # Attempt to remove head or rear + >>> dll.rear + Node: key: None, val: None, freq: 0, has next: False, has prev: True + >>> dll.remove(dll.rear) is None + True + + """ - def __init__(self): - self.head = DoubleLinkedListNode(None, None) - self.rear = DoubleLinkedListNode(None, None) + def __init__(self) -> None: + self.head: DoubleLinkedListNode[T, U] = DoubleLinkedListNode(None, None) + self.rear: DoubleLinkedListNode[T, U] = DoubleLinkedListNode(None, None) self.head.next, self.rear.prev = self.rear, self.head - def add(self, node: DoubleLinkedListNode) -> None: + def __repr__(self) -> str: + rep = ["DoubleLinkedList"] + node = self.head + while node.next is not None: + rep.append(str(node)) + node = node.next + rep.append(str(self.rear)) + return ",\n ".join(rep) + + def add(self, node: DoubleLinkedListNode[T, U]) -> None: """ - Adds the given node at the head of the list and shifting it to proper position + Adds the given node at the tail of the list and shifting it to proper position """ - temp = self.rear.prev + previous = self.rear.prev - self.rear.prev, node.next = node, self.rear - temp.next, node.prev = node, temp + # All nodes other than self.head are guaranteed to have non-None previous + assert previous is not None + + previous.next = node + node.prev = previous + self.rear.prev = node + node.next = self.rear node.freq += 1 self._position_node(node) - def _position_node(self, node: DoubleLinkedListNode) -> None: - while node.prev.key and node.prev.freq > node.freq: - node1, node2 = node, node.prev - node1.prev, node2.next = node2.prev, node1.prev - node1.next, node2.prev = node2, node1 + def _position_node(self, node: DoubleLinkedListNode[T, U]) -> None: + """ + Moves node forward to maintain invariant of sort by freq value + """ - def remove(self, node: DoubleLinkedListNode) -> DoubleLinkedListNode: + while node.prev is not None and node.prev.freq > node.freq: + # swap node with previous node + previous_node = node.prev + + node.prev = previous_node.prev + previous_node.next = node.prev + node.next = previous_node + previous_node.prev = node + + def remove( + self, node: DoubleLinkedListNode[T, U] + ) -> DoubleLinkedListNode[T, U] | None: """ Removes and returns the given node from the list + + Returns None if node.prev or node.next is None """ - temp_last, temp_next = node.prev, node.next - node.prev, node.next = None, None - temp_last.next, temp_next.prev = temp_next, temp_last + if node.prev is None or node.next is None: + return None + + node.prev.next = node.next + node.next.prev = node.prev + node.prev = None + node.next = None return node -class LFUCache: +class LFUCache(Generic[T, U]): """ LFU Cache to store a given capacity of data. Can be used as a stand-alone object or as a function decorator. @@ -66,9 +170,11 @@ class LFUCache: >>> cache.get(1) 1 >>> cache.set(3, 3) - >>> cache.get(2) # None is returned + >>> cache.get(2) is None + True >>> cache.set(4, 4) - >>> cache.get(1) # None is returned + >>> cache.get(1) is None + True >>> cache.get(3) 3 >>> cache.get(4) @@ -89,15 +195,15 @@ class LFUCache: """ # class variable to map the decorator functions to their respective instance - decorator_function_to_instance_map = {} + decorator_function_to_instance_map: dict[Callable[[T], U], LFUCache[T, U]] = {} def __init__(self, capacity: int): - self.list = DoubleLinkedList() + self.list: DoubleLinkedList[T, U] = DoubleLinkedList() self.capacity = capacity self.num_keys = 0 self.hits = 0 self.miss = 0 - self.cache = {} + self.cache: dict[T, DoubleLinkedListNode[T, U]] = {} def __repr__(self) -> str: """ @@ -110,40 +216,58 @@ def __repr__(self) -> str: f"capacity={self.capacity}, current_size={self.num_keys})" ) - def __contains__(self, key: int) -> bool: + def __contains__(self, key: T) -> bool: """ >>> cache = LFUCache(1) + >>> 1 in cache False + >>> cache.set(1, 1) >>> 1 in cache True """ + return key in self.cache - def get(self, key: int) -> int | None: + def get(self, key: T) -> U | None: """ Returns the value for the input key and updates the Double Linked List. Returns - None if key is not present in cache + Returns None if key is not present in cache """ if key in self.cache: self.hits += 1 - self.list.add(self.list.remove(self.cache[key])) - return self.cache[key].val + value_node: DoubleLinkedListNode[T, U] = self.cache[key] + node = self.list.remove(self.cache[key]) + assert node == value_node + + # node is guaranteed not None because it is in self.cache + assert node is not None + self.list.add(node) + return node.val self.miss += 1 return None - def set(self, key: int, value: int) -> None: + def set(self, key: T, value: U) -> None: """ Sets the value for the input key and updates the Double Linked List """ if key not in self.cache: if self.num_keys >= self.capacity: - key_to_delete = self.list.head.next.key - self.list.remove(self.cache[key_to_delete]) - del self.cache[key_to_delete] + # delete first node when over capacity + first_node = self.list.head.next + + # guaranteed to have a non-None first node when num_keys > 0 + # explain to type checker via assertions + assert first_node is not None + assert first_node.key is not None + assert ( + self.list.remove(first_node) is not None + ) # node guaranteed to be in list assert node.key is not None + + del self.cache[first_node.key] self.num_keys -= 1 self.cache[key] = DoubleLinkedListNode(key, value) self.list.add(self.cache[key]) @@ -151,32 +275,35 @@ def set(self, key: int, value: int) -> None: else: node = self.list.remove(self.cache[key]) + assert node is not None # node guaranteed to be in list node.val = value self.list.add(node) - @staticmethod - def decorator(size: int = 128): + @classmethod + def decorator( + cls, size: int = 128 + ) -> Callable[[Callable[[T], U]], Callable[..., U]]: """ Decorator version of LFU Cache + + Decorated function must be function of T -> U """ - def cache_decorator_inner(func: Callable): - def cache_decorator_wrapper(*args, **kwargs): - if func not in LFUCache.decorator_function_to_instance_map: - LFUCache.decorator_function_to_instance_map[func] = LFUCache(size) + def cache_decorator_inner(func: Callable[[T], U]) -> Callable[..., U]: + def cache_decorator_wrapper(*args: T) -> U: + if func not in cls.decorator_function_to_instance_map: + cls.decorator_function_to_instance_map[func] = LFUCache(size) - result = LFUCache.decorator_function_to_instance_map[func].get(args[0]) + result = cls.decorator_function_to_instance_map[func].get(args[0]) if result is None: - result = func(*args, **kwargs) - LFUCache.decorator_function_to_instance_map[func].set( - args[0], result - ) + result = func(*args) + cls.decorator_function_to_instance_map[func].set(args[0], result) return result - def cache_info(): - return LFUCache.decorator_function_to_instance_map[func] + def cache_info() -> LFUCache[T, U]: + return cls.decorator_function_to_instance_map[func] - cache_decorator_wrapper.cache_info = cache_info + setattr(cache_decorator_wrapper, "cache_info", cache_info) return cache_decorator_wrapper From 18390e3c1f1b7f8ad628bbf73847ea9eb4b3ac09 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Wed, 10 Nov 2021 13:47:32 -0800 Subject: [PATCH 12/14] [mypy] annotates cls var in other/lfu_cache --- other/lfu_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/other/lfu_cache.py b/other/lfu_cache.py index 200f388af11a..37cef7003ad2 100644 --- a/other/lfu_cache.py +++ b/other/lfu_cache.py @@ -281,7 +281,7 @@ def set(self, key: T, value: U) -> None: @classmethod def decorator( - cls, size: int = 128 + cls: type[LFUCache[T, U]], size: int = 128 ) -> Callable[[Callable[[T], U]], Callable[..., U]]: """ Decorator version of LFU Cache From 1e97d44db80849dc9682c4f25fba5541c20d5736 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Wed, 10 Nov 2021 13:48:41 -0800 Subject: [PATCH 13/14] cleans up items from code review for lfu_cache and lru_cache --- other/lfu_cache.py | 5 ++--- other/lru_cache.py | 12 ++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/other/lfu_cache.py b/other/lfu_cache.py index 37cef7003ad2..e955973c95b0 100644 --- a/other/lfu_cache.py +++ b/other/lfu_cache.py @@ -263,9 +263,8 @@ def set(self, key: T, value: U) -> None: # explain to type checker via assertions assert first_node is not None assert first_node.key is not None - assert ( - self.list.remove(first_node) is not None - ) # node guaranteed to be in list assert node.key is not None + assert self.list.remove(first_node) is not None + # first_node guaranteed to be in list del self.cache[first_node.key] self.num_keys -= 1 diff --git a/other/lru_cache.py b/other/lru_cache.py index 1ebf4308dbec..98051f89db4f 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -10,8 +10,7 @@ class DoubleLinkedListNode(Generic[T, U]): """ Double Linked List Node built specifically for LRU Cache - >>> node = DoubleLinkedListNode(1,1) - >>> node + >>> DoubleLinkedListNode(1,1) Node: key: 1, val: 1, has next: False, has prev: False """ @@ -161,9 +160,9 @@ class LRUCache(Generic[T, U]): Node: key: 1, val: 1, has next: True, has prev: True, Node: key: None, val: None, has next: False, has prev: True - >>> cache.cache + >>> cache.cache # doctest: +NORMALIZE_WHITESPACE {1: Node: key: 1, val: 1, has next: True, has prev: True, \ -2: Node: key: 2, val: 2, has next: True, has prev: True} + 2: Node: key: 2, val: 2, has next: True, has prev: True} >>> cache.set(3, 3) @@ -174,9 +173,9 @@ class LRUCache(Generic[T, U]): Node: key: 3, val: 3, has next: True, has prev: True, Node: key: None, val: None, has next: False, has prev: True - >>> cache.cache + >>> cache.cache # doctest: +NORMALIZE_WHITESPACE {1: Node: key: 1, val: 1, has next: True, has prev: True, \ -3: Node: key: 3, val: 3, has next: True, has prev: True} + 3: Node: key: 3, val: 3, has next: True, has prev: True} >>> cache.get(2) is None True @@ -250,6 +249,7 @@ def get(self, key: T) -> U | None: Returns the value for the input key and updates the Double Linked List. Returns None if key is not present in cache """ + # Note: pythonic interface would throw KeyError rather than return None if key in self.cache: self.hits += 1 From 6de578adabb1ba122cfbe3f49b2d8fa816a759da Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Wed, 10 Nov 2021 13:52:02 -0800 Subject: [PATCH 14/14] [mypy] runs mypy on lfu_cache and lru_cache --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index f00b3eeb6bac..7dbc7c4ffc80 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,4 +2,4 @@ ignore_missing_imports = True install_types = True non_interactive = True -exclude = (other/least_recently_used.py|other/lfu_cache.py|other/lru_cache.py) +exclude = (other/least_recently_used.py)