From 1c2b85c4cf75c620ecea2637f9fc9d47fabcfc09 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 15 Jun 2021 01:51:18 -0500 Subject: [PATCH 1/3] bpo-44310: Add a FAQ entry for caching method calls --- Doc/faq/programming.rst | 96 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/Doc/faq/programming.rst b/Doc/faq/programming.rst index 921357d8fcdd2c..002022aa8dd1d6 100644 --- a/Doc/faq/programming.rst +++ b/Doc/faq/programming.rst @@ -1826,6 +1826,102 @@ For example, here is the implementation of return True return False +How do I cache method calls? +---------------------------- + +The two principal tools for caching methods are :functools:`cached_property` +and :functools:`lru_cache`. The former stores results at the instance level +and the latter at the class level. + +The *cached_property* approach only works with methods that do not take +any arguments. It does not create a reference to the instance. The +cached method result will be kept only as long as the instance is alive. + +The advantage is that when an instance is not longer used, the cached +method result will be released right away. The disadvantage is that if +instances accumulate, so too will the accumulated method results. They +can grow without bound. + +The *lru_cache* approach works with methods that have hashable +arguments. It creates a reference to the instance unless special +efforts are made to pass in weak references. + +The advantage of the least recently used algorithm is that the cache is +bounded by the specified *maxsize*. The disadvantage is that instances +are kept alive until they age out of the cache or until the cache is +cleared. + +To avoid keeping an instance alive, it can be wrapped a weak reference +proxy. That allows an instance to be freed prior aging out of the LRU +cache. That said, the weak reference technique is rarely needed. It is +only helpful when the instances hold large amounts of data and the +normal aging-out process isn't fast enough. And even though the +instance is released early, the cache still keeps references to the +other method arguments and to the result of the method call. + +This example shows the various techniques:: + + class Weather: + "Lookup weather information on a government website" + + def __init__(self, station_id): + self._station_id = station_id + # The _station_id is private and immutable + + def current_temperature(self): + "Latest hourly observation" + # Do not cache this because old results + # can be out of date. + + @cached_property + def location(self): + "Return the longitude/latitude coordinates of the station" + # Result only depends on the station_id + + @lru_cache(maxsize=20) + def historic_rainfall(self, date, units='mm'): + "Rainfall on a given date" + # Depends on the station_id, date, and units. + + def climate(self, category='average_temperature'): + return self._climate(weakref.proxy(self), category) + + @staticmethod + @lru_cache(maxsize=10) + def _climate(self_proxy, category): + "List of daily average temperatures for a full year" + # Depends on a weak reference to the instance + # and on the category parameter. + +The above example assumes that the *station_id* never changes. If the +relevant instance attributes are mutable, the *cached_property* approach +can't be made to work because it cannot detect changes to the +attributes. + +The *lru_cache* approach can be made to work, but the class needs to +define the *__eq__* and *__hash__* methods so that the cache can detect +when relevant attributes have been updated:: + + class Weather: + "Example with a mutable station identifier" + + def __init__(self, station_id): + self.station_id = station_id + + def change_station(self, station_id): + self.station_id = station_id + + def __eq__(self, other): + return self.station_id == other.station_id + + def __hash__(self): + return hash(self.station_id) + + @lru_cache(maxsize=20) + def historic_rainfall(self, date, units='cm'): + 'Rainfall on a given date' + # Depends on the station_id, date, and units. + Modules ======= From 3aeef243e1b5bbf465a05d591015e807ce70fcc7 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 15 Jun 2021 05:29:22 -0500 Subject: [PATCH 2/3] Fix markup --- Doc/faq/programming.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Doc/faq/programming.rst b/Doc/faq/programming.rst index 002022aa8dd1d6..7174763e4827a3 100644 --- a/Doc/faq/programming.rst +++ b/Doc/faq/programming.rst @@ -1829,9 +1829,10 @@ For example, here is the implementation of How do I cache method calls? ---------------------------- -The two principal tools for caching methods are :functools:`cached_property` -and :functools:`lru_cache`. The former stores results at the instance level -and the latter at the class level. +The two principal tools for caching methods are +:func:`functools.cached_property` and :func:`functools.lru_cache`. The +former stores results at the instance level and the latter at the class +level. The *cached_property* approach only works with methods that do not take any arguments. It does not create a reference to the instance. The @@ -1898,9 +1899,9 @@ relevant instance attributes are mutable, the *cached_property* approach can't be made to work because it cannot detect changes to the attributes. -The *lru_cache* approach can be made to work, but the class needs to -define the *__eq__* and *__hash__* methods so that the cache can detect -when relevant attributes have been updated:: +The *lru_cache* approach can be made to work, but the class needs to define the +*__eq__* and *__hash__* methods so the cache can detect relevant attribute +updates:: class Weather: "Example with a mutable station identifier" From 16ef55944ec5afaa1eed9bde454098c0cf75febd Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 15 Jun 2021 10:51:26 -0500 Subject: [PATCH 3/3] Move docstring to the public method --- Doc/faq/programming.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/faq/programming.rst b/Doc/faq/programming.rst index 7174763e4827a3..a519275040491d 100644 --- a/Doc/faq/programming.rst +++ b/Doc/faq/programming.rst @@ -1885,12 +1885,12 @@ This example shows the various techniques:: # Depends on the station_id, date, and units. def climate(self, category='average_temperature'): + "List of daily average temperatures for a full year" return self._climate(weakref.proxy(self), category) @staticmethod @lru_cache(maxsize=10) def _climate(self_proxy, category): - "List of daily average temperatures for a full year" # Depends on a weak reference to the instance # and on the category parameter.