From cbaafee4414e91de404aeb5d7ccb796c64f8431f Mon Sep 17 00:00:00 2001 From: francois-durand Date: Thu, 5 Mar 2020 09:48:24 +0100 Subject: [PATCH 1/4] Access or delete all cached properties * All cached properties derive from a parent class ``CachedProperty``, so that we can use isinstance to identify them. * Add function ``cached_properties``: iterate over the cached properties of an object. * Add function ``cached_properties_computed``: iterate over the cached properties that are already computed. * Add function ``delete_cache``: empty the whole cache of an object. * Add decorator ``property_deleting_cache``: a property that deletes the cache when it is set or deleted. --- cached_property.py | 217 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 214 insertions(+), 3 deletions(-) diff --git a/cached_property.py b/cached_property.py index 3e530fc..865c191 100644 --- a/cached_property.py +++ b/cached_property.py @@ -15,7 +15,13 @@ asyncio = None -class cached_property(object): +class CachedProperty(object): + """Parent class for cached properties.""" + # As of now, it is only used for ``isinstance`` tests. Cf. ``cached_properties_names``. + pass + + +class cached_property(CachedProperty): """ A property that is only computed once per instance and then replaces itself with an ordinary attribute. Deleting the attribute resets the property. @@ -47,7 +53,7 @@ def wrapper(): return wrapper() -class threaded_cached_property(object): +class threaded_cached_property(CachedProperty): """ A cached_property version for use in environments where multiple threads might concurrently try to access the property. @@ -74,7 +80,7 @@ def __get__(self, obj, cls): return obj_dict.setdefault(name, self.func(obj)) -class cached_property_with_ttl(object): +class cached_property_with_ttl(CachedProperty): """ A property that is only computed once per instance and then replaces itself with an ordinary attribute. Setting the ttl to a number expresses how long @@ -151,3 +157,208 @@ def __get__(self, obj, cls): # Alias to make threaded_cached_property_with_ttl easier to use threaded_cached_property_ttl = threaded_cached_property_with_ttl timed_threaded_cached_property = threaded_cached_property_with_ttl + + +def all_members(cls): + """All members of a class. + + Credit: Jürgen Hermann, Alex Martelli. From + https://www.oreilly.com/library/view/python-cookbook/0596001673/ch05s03.html. + + + Parameters + ---------- + cls : class + + Returns + ------- + dict + Similar to ``cls.__dict__``, but include also the inherited members. + + Examples + -------- + >>> class Parent(object): + ... attribute_parent = 42 + >>> class Child(Parent): + ... attribute_child = 51 + >>> 'attribute_child' in all_members(Child).keys() + True + >>> 'attribute_parent' in all_members(Child).keys() + True + """ + try: + # Try getting all relevant classes in method-resolution order + mro = list(cls.__mro__) + except AttributeError: + # If a class has no _ _mro_ _, then it's a classic class + def getmro(a_class, recurse): + an_mro = [a_class] + for base in a_class.__bases__: + an_mro.extend(recurse(base, recurse)) + return an_mro + mro = getmro(cls, getmro) + mro.reverse() + members = {} + for someClass in mro: + members.update(vars(someClass)) + return members + + +def cached_properties(o): + """Cached properties (whether already computed or not). + + Parameters + ---------- + o : object + + Yields + ------ + str + Name of each cached property. + + Examples + -------- + >>> class MyClass(object): + ... @cached_property + ... def my_cached_property(self): + ... print('Computing my_cached_property...') + ... return 2 + ... @cached_property + ... def my_second_cached_property(self): + ... print('Computing my_second_cached_property...') + ... return 3 + >>> my_object = MyClass() + >>> my_object.my_cached_property + Computing my_cached_property... + 2 + >>> for name in cached_properties(my_object): + ... print(name) + my_cached_property + my_second_cached_property + """ + return (k for k, v in all_members(o.__class__).items() + if isinstance(v, CachedProperty)) + + +def cached_properties_computed(o): + """Cached properties that are already computed. + + Parameters + ---------- + o : object + + Yields + ------ + str + Name of each cached property that is already computed. + + Examples + -------- + >>> class MyClass(object): + ... @cached_property + ... def my_cached_property(self): + ... print('Computing my_cached_property...') + ... return 2 + ... @cached_property + ... def my_second_cached_property(self): + ... print('Computing my_second_cached_property...') + ... return 3 + >>> my_object = MyClass() + >>> my_object.my_cached_property + Computing my_cached_property... + 2 + >>> for name in cached_properties_computed(my_object): + ... print(name) + my_cached_property + """ + return (k for k in cached_properties(o) if k in o.__dict__.keys()) + + +def delete_cache(o): + """Delete the cache. + + Parameters + ---------- + o : object + + Examples + -------- + >>> class MyClass(object): + ... @cached_property + ... def my_cached_property(self): + ... print('Computing my_cached_property...') + ... return 2 + ... @cached_property + ... def my_second_cached_property(self): + ... print('Computing my_second_cached_property...') + ... return 3 + >>> my_object = MyClass() + >>> my_object.my_cached_property + Computing my_cached_property... + 2 + >>> delete_cache(my_object) + >>> my_object.my_cached_property + Computing my_cached_property... + 2 + """ + for cached_property_name in cached_properties(o): + try: + del o.__dict__[cached_property_name] + except KeyError: + pass + + +class property_deleting_cache: + """Define a property that deletes the cache when set or deleted. + + Parameters + ---------- + func : function + The code of the function is not used, only its docstring. + + Returns + ------- + property + A property with get, set and delete. When it is set or deleted, + it deletes the cache of the object it belongs to. + + Examples + -------- + >>> class MyClass(object): + ... def __init__(self, my_parameter): + ... self.my_parameter = my_parameter + ... @property_deleting_cache + ... def my_parameter(self): + ... "A parameter that deletes the cache when set or deleted." + ... pass + ... @cached_property + ... def my_cached_property(self): + ... print('Computing my_cached_property...') + ... return self.my_parameter + 1 + >>> my_object = MyClass(my_parameter=41) + >>> my_object.my_cached_property + Computing my_cached_property... + 42 + >>> my_object.my_parameter = 50 + >>> my_object.my_cached_property + Computing my_cached_property... + 51 + >>> MyClass.my_parameter.__doc__ + 'A parameter that deletes the cache when set or deleted.' + """ + def __init__(self, func): + self.__doc__ = getattr(func, "__doc__") + self.func = func + + def __get__(self, obj, cls): + if obj is None: + return self + return self.value + + def __set__(self, obj, value): + delete_cache(obj) + self.value = value + + def __delete__(self, obj): + delete_cache(obj) + del self.value From 874e4653471e5c4edaf88da9dd8e1e0ad57cdbc6 Mon Sep 17 00:00:00 2001 From: francois-durand Date: Thu, 5 Mar 2020 10:30:28 +0100 Subject: [PATCH 2/4] Alternative syntax for property_deleting_cache * Use Black. * ``property_deleting_cache`` uses the function for its side effects, even if its return value is ignored. * ``property_deleting_cache_2``: proposition or alternate syntax. --- cached_property.py | 73 +++++++++++++++++++++++++++++++++++++++------- conftest.py | 1 - 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/cached_property.py b/cached_property.py index 865c191..34bfeb7 100644 --- a/cached_property.py +++ b/cached_property.py @@ -17,6 +17,7 @@ class CachedProperty(object): """Parent class for cached properties.""" + # As of now, it is only used for ``isinstance`` tests. Cf. ``cached_properties_names``. pass @@ -196,6 +197,7 @@ def getmro(a_class, recurse): for base in a_class.__bases__: an_mro.extend(recurse(base, recurse)) return an_mro + mro = getmro(cls, getmro) mro.reverse() members = {} @@ -236,8 +238,9 @@ def cached_properties(o): my_cached_property my_second_cached_property """ - return (k for k, v in all_members(o.__class__).items() - if isinstance(v, CachedProperty)) + return ( + k for k, v in all_members(o.__class__).items() if isinstance(v, CachedProperty) + ) def cached_properties_computed(o): @@ -309,18 +312,14 @@ def delete_cache(o): class property_deleting_cache: - """Define a property that deletes the cache when set or deleted. + """A property that deletes the cache when set or deleted. Parameters ---------- func : function - The code of the function is not used, only its docstring. - - Returns - ------- - property - A property with get, set and delete. When it is set or deleted, - it deletes the cache of the object it belongs to. + Each time we get the property, this function is run for the sake of + its side effects (but its return value is ignored); then the value of + the property is accessed. Examples -------- @@ -330,7 +329,7 @@ class property_deleting_cache: ... @property_deleting_cache ... def my_parameter(self): ... "A parameter that deletes the cache when set or deleted." - ... pass + ... print('Accessing my_parameter...') ... @cached_property ... def my_cached_property(self): ... print('Computing my_cached_property...') @@ -338,18 +337,70 @@ class property_deleting_cache: >>> my_object = MyClass(my_parameter=41) >>> my_object.my_cached_property Computing my_cached_property... + Accessing my_parameter... 42 >>> my_object.my_parameter = 50 >>> my_object.my_cached_property Computing my_cached_property... + Accessing my_parameter... 51 >>> MyClass.my_parameter.__doc__ 'A parameter that deletes the cache when set or deleted.' """ + def __init__(self, func): self.__doc__ = getattr(func, "__doc__") self.func = func + def __get__(self, obj, cls): + if obj is None: + return self + self.func(obj) + return self.value + + def __set__(self, obj, value): + delete_cache(obj) + self.value = value + + def __delete__(self, obj): + delete_cache(obj) + del self.value + + +class property_deleting_cache_2: + """Define a property that deletes the cache when set or deleted. + + Parameters + ---------- + doc : str + The documentation of the property + + Examples + -------- + >>> class MyClass(object): + ... def __init__(self, my_parameter): + ... self.my_parameter = my_parameter + ... my_parameter = property_deleting_cache_2( + ... doc="A parameter that deletes the cache when set or deleted.") + ... @cached_property + ... def my_cached_property(self): + ... print('Computing my_cached_property...') + ... return self.my_parameter + 1 + >>> my_object = MyClass(my_parameter=41) + >>> my_object.my_cached_property + Computing my_cached_property... + 42 + >>> my_object.my_parameter = 50 + >>> my_object.my_cached_property + Computing my_cached_property... + 51 + >>> MyClass.my_parameter.__doc__ + 'A parameter that deletes the cache when set or deleted.' + """ + + def __init__(self, doc): + self.__doc__ = doc + def __get__(self, obj, cls): if obj is None: return self diff --git a/conftest.py b/conftest.py index 0563f64..d67efea 100644 --- a/conftest.py +++ b/conftest.py @@ -1,4 +1,3 @@ - import sys # Whether "import asyncio" works From dafc574f22203296bac50ada9141ebee32a38f45 Mon Sep 17 00:00:00 2001 From: francois-durand Date: Thu, 5 Mar 2020 11:09:20 +0100 Subject: [PATCH 3/4] Add ``is_cached`` and ``un_cache`` * Remove ``cached_properties_computed``, because it is now a trivial combination of ``cached_properties`` and ``is_cached``. --- cached_property.py | 108 +++++++++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 37 deletions(-) diff --git a/cached_property.py b/cached_property.py index 34bfeb7..7b04e00 100644 --- a/cached_property.py +++ b/cached_property.py @@ -160,6 +160,73 @@ def __get__(self, obj, cls): timed_threaded_cached_property = threaded_cached_property_with_ttl +def is_cached(o, name): + """Whether a cached property is already computed. + + Parameters + ---------- + o : object + The object the property belongs to. + name : str + Name of the property. + + Returns + ------- + bool + True iff the property is already computed. + + Examples + -------- + >>> class MyClass(object): + ... @cached_property + ... def my_cached_property(self): + ... print('Computing my_cached_property...') + ... return 42 + ... @cached_property + ... def my_second_cached_property(self): + ... print('Computing my_second_cached_property...') + ... return 51 + >>> my_object = MyClass() + >>> my_object.my_cached_property + Computing my_cached_property... + 42 + >>> is_cached(my_object, 'my_cached_property') + True + >>> is_cached(my_object, 'my_second_cached_property') + False + """ + return name in o.__dict__ + + +def un_cache(o, name): + """Empty the cache of a cached property. + + Parameters + ---------- + o : object + The object the property belongs to. + name : str + Name of the property. + + Examples + -------- + >>> class MyClass(object): + ... @cached_property + ... def my_cached_property(self): + ... print('Computing my_cached_property...') + ... return 42 + >>> my_object = MyClass() + >>> my_object.my_cached_property + Computing my_cached_property... + 42 + >>> un_cache(my_object, 'my_cached_property') + >>> my_object.my_cached_property + Computing my_cached_property... + 42 + """ + del o.__dict__[name] + + def all_members(cls): """All members of a class. @@ -239,46 +306,13 @@ def cached_properties(o): my_second_cached_property """ return ( - k for k, v in all_members(o.__class__).items() if isinstance(v, CachedProperty) + k for k, v in all_members(o.__class__).items() + if isinstance(v, CachedProperty) ) -def cached_properties_computed(o): - """Cached properties that are already computed. - - Parameters - ---------- - o : object - - Yields - ------ - str - Name of each cached property that is already computed. - - Examples - -------- - >>> class MyClass(object): - ... @cached_property - ... def my_cached_property(self): - ... print('Computing my_cached_property...') - ... return 2 - ... @cached_property - ... def my_second_cached_property(self): - ... print('Computing my_second_cached_property...') - ... return 3 - >>> my_object = MyClass() - >>> my_object.my_cached_property - Computing my_cached_property... - 2 - >>> for name in cached_properties_computed(my_object): - ... print(name) - my_cached_property - """ - return (k for k in cached_properties(o) if k in o.__dict__.keys()) - - def delete_cache(o): - """Delete the cache. + """Empty the cache of a whole object. Parameters ---------- @@ -306,7 +340,7 @@ def delete_cache(o): """ for cached_property_name in cached_properties(o): try: - del o.__dict__[cached_property_name] + un_cache(o, cached_property_name) except KeyError: pass From 47b2b922b60172d91670644bc4b8f97854da917f Mon Sep 17 00:00:00 2001 From: francois-durand Date: Thu, 5 Mar 2020 17:43:22 +0100 Subject: [PATCH 4/4] Update Readme * Update Readme: * Add new functions (un_cache, delete_cache, etc.). * Replace the monopoly running example by a more generic example. * Add a notebook "readme companion" to test the code of the readme. --- .../readme_companion-checkpoint.ipynb | 1080 +++++++++++++++++ README.rst | 300 +++-- readme_companion.ipynb | 1080 +++++++++++++++++ 3 files changed, 2358 insertions(+), 102 deletions(-) create mode 100644 .ipynb_checkpoints/readme_companion-checkpoint.ipynb create mode 100644 readme_companion.ipynb diff --git a/.ipynb_checkpoints/readme_companion-checkpoint.ipynb b/.ipynb_checkpoints/readme_companion-checkpoint.ipynb new file mode 100644 index 0000000..8dd32ae --- /dev/null +++ b/.ipynb_checkpoints/readme_companion-checkpoint.ipynb @@ -0,0 +1,1080 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to use it" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.866135Z", + "start_time": "2020-03-05T16:07:40.860150Z" + } + }, + "outputs": [], + "source": [ + "class MyClass(object):\n", + "\n", + " @property\n", + " def my_property(self):\n", + " # In reality, this might represent a database call or time\n", + " # intensive task like calling a third-party API.\n", + " print('Computing my_property...') \n", + " return 42" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.896052Z", + "start_time": "2020-03-05T16:07:40.870124Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object = MyClass()\n", + "my_object.my_property" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.904031Z", + "start_time": "2020-03-05T16:07:40.898047Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_property" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.912050Z", + "start_time": "2020-03-05T16:07:40.906027Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import cached_property\n", + "\n", + "class MyClass(object):\n", + "\n", + " @cached_property\n", + " def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return 42" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.923034Z", + "start_time": "2020-03-05T16:07:40.914005Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object = MyClass()\n", + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.930992Z", + "start_time": "2020-03-05T16:07:40.924977Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Inspecting the cache" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.940933Z", + "start_time": "2020-03-05T16:07:40.932954Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import cached_property, cached_properties, is_cached" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.948911Z", + "start_time": "2020-03-05T16:07:40.942927Z" + } + }, + "outputs": [], + "source": [ + "class MyClass(object):\n", + "\n", + " @cached_property\n", + " def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return 42\n", + " \n", + " @cached_property\n", + " def my_second_cached_property(self):\n", + " print('Computing my_second_cached_property...')\n", + " return 51" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.956891Z", + "start_time": "2020-03-05T16:07:40.950906Z" + } + }, + "outputs": [], + "source": [ + "my_object = MyClass()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.965899Z", + "start_time": "2020-03-05T16:07:40.958885Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "my_cached_property\n", + "my_second_cached_property\n" + ] + } + ], + "source": [ + "for property_name in cached_properties(my_object):\n", + " print(property_name)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.977835Z", + "start_time": "2020-03-05T16:07:40.967861Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.985813Z", + "start_time": "2020-03-05T16:07:40.978832Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "is_cached(my_object, 'my_cached_property')" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:40.997781Z", + "start_time": "2020-03-05T16:07:40.988807Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "is_cached(my_object, 'my_second_cached_property')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Invalidating the cache" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.005760Z", + "start_time": "2020-03-05T16:07:40.998778Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import cached_property, un_cache, delete_cache" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.015734Z", + "start_time": "2020-03-05T16:07:41.007754Z" + } + }, + "outputs": [], + "source": [ + "class MyClass(object):\n", + "\n", + " @cached_property\n", + " def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return 42\n", + " \n", + " @cached_property\n", + " def my_second_cached_property(self):\n", + " print('Computing my_second_cached_property...')\n", + " return 51" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.025707Z", + "start_time": "2020-03-05T16:07:41.017728Z" + } + }, + "outputs": [], + "source": [ + "my_object = MyClass()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.035680Z", + "start_time": "2020-03-05T16:07:41.026705Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.044655Z", + "start_time": "2020-03-05T16:07:41.037687Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_second_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "51" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_second_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.054632Z", + "start_time": "2020-03-05T16:07:41.047648Z" + } + }, + "outputs": [], + "source": [ + "un_cache(my_object, 'my_cached_property')" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.067596Z", + "start_time": "2020-03-05T16:07:41.058620Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.077568Z", + "start_time": "2020-03-05T16:07:41.069590Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "51" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_second_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.086583Z", + "start_time": "2020-03-05T16:07:41.079562Z" + } + }, + "outputs": [], + "source": [ + "delete_cache(my_object)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.097515Z", + "start_time": "2020-03-05T16:07:41.088538Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.107488Z", + "start_time": "2020-03-05T16:07:41.098512Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_second_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "51" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_second_cached_property" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Property deleting the cache" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.115466Z", + "start_time": "2020-03-05T16:07:41.108485Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import cached_property, property_deleting_cache" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.124442Z", + "start_time": "2020-03-05T16:07:41.116463Z" + } + }, + "outputs": [], + "source": [ + "class MyClass(object):\n", + "\n", + " def __init__(self, my_parameter):\n", + " self.my_parameter = my_parameter\n", + " \n", + " @property_deleting_cache\n", + " def my_parameter(self):\n", + " \"A parameter that deletes the cache when set or deleted.\"\n", + " print('Accessing my_parameter...')\n", + " \n", + " @cached_property\n", + " def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return self.my_parameter + 1" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.136422Z", + "start_time": "2020-03-05T16:07:41.125440Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n", + "Accessing my_parameter...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object = MyClass(my_parameter=41)\n", + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.145387Z", + "start_time": "2020-03-05T16:07:41.138405Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.154370Z", + "start_time": "2020-03-05T16:07:41.146384Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n", + "Accessing my_parameter...\n" + ] + }, + { + "data": { + "text/plain": [ + "51" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_parameter = 50\n", + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.163339Z", + "start_time": "2020-03-05T16:07:41.156358Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "51" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Working with Threads" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.172316Z", + "start_time": "2020-03-05T16:07:41.165334Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import threaded_cached_property\n", + "\n", + "class MyClass(object):\n", + "\n", + " @threaded_cached_property\n", + " def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return 42" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.187282Z", + "start_time": "2020-03-05T16:07:41.173313Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + } + ], + "source": [ + "from threading import Thread\n", + "my_object = MyClass()\n", + "threads = []\n", + "for x in range(10):\n", + " thread = Thread(target=lambda: my_object.my_cached_property)\n", + " thread.start()\n", + " threads.append(thread)\n", + "\n", + "for thread in threads:\n", + " thread.join()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Working with async/await (Python 3.5+)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.194255Z", + "start_time": "2020-03-05T16:07:41.189269Z" + } + }, + "outputs": [], + "source": [ + "# This is just a trick to make asyncio work in jupyter.\n", + "# Cf. https://markhneedham.com/blog/2019/05/10/jupyter-runtimeerror-this-event-loop-is-already-running/\n", + "import nest_asyncio\n", + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.202234Z", + "start_time": "2020-03-05T16:07:41.195252Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import cached_property\n", + "\n", + "class MyClass(object):\n", + "\n", + " @cached_property\n", + " async def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return 42" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.213205Z", + "start_time": "2020-03-05T16:07:41.204229Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n", + "42\n", + "42\n", + "42\n" + ] + } + ], + "source": [ + "async def print_my_cached_property():\n", + " my_object = MyClass()\n", + " print(await my_object.my_cached_property)\n", + " print(await my_object.my_cached_property)\n", + " print(await my_object.my_cached_property)\n", + "import asyncio\n", + "asyncio.get_event_loop().run_until_complete(print_my_cached_property())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Timing out the cache" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.221185Z", + "start_time": "2020-03-05T16:07:41.215199Z" + } + }, + "outputs": [], + "source": [ + "import random\n", + "from cached_property import cached_property_with_ttl\n", + "\n", + "class MyClass(object):\n", + "\n", + " @cached_property_with_ttl(ttl=2) # cache invalidates after 2 seconds\n", + " def my_cached_property(self):\n", + " return random.random()" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.230161Z", + "start_time": "2020-03-05T16:07:41.223179Z" + } + }, + "outputs": [], + "source": [ + "my_object = MyClass()" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.242128Z", + "start_time": "2020-03-05T16:07:41.232154Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.4770410300930312" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:41.255093Z", + "start_time": "2020-03-05T16:07:41.244123Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.4770410300930312" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:44.264416Z", + "start_time": "2020-03-05T16:07:41.257088Z" + } + }, + "outputs": [], + "source": [ + "from time import sleep\n", + "sleep(3) # Sleeps long enough to expire the cache" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:07:44.287350Z", + "start_time": "2020-03-05T16:07:44.272390Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.259086428146758" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": false, + "sideBar": true, + "skip_h1_title": true, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/README.rst b/README.rst index 2ceb7ac..7864d2a 100644 --- a/README.rst +++ b/README.rst @@ -25,129 +25,229 @@ Why? How to use it -------------- -Let's define a class with an expensive property. Every time you stay there the -price goes up by $50! +Let's define a class with an expensive property. Imagine that the "print" +instruction in this example represents a long computation: .. code-block:: python - class Monopoly(object): - - def __init__(self): - self.boardwalk_price = 500 + class MyClass(object): @property - def boardwalk(self): + def my_property(self): # In reality, this might represent a database call or time # intensive task like calling a third-party API. - self.boardwalk_price += 50 - return self.boardwalk_price + print('Computing my_property...') + return 42 Now run it: .. code-block:: python - >>> monopoly = Monopoly() - >>> monopoly.boardwalk - 550 - >>> monopoly.boardwalk - 600 + >>> my_object = MyClass() + >>> my_object.my_property + Computing my_property... + 42 + >>> my_object.my_property + Computing my_property... + 42 + +Let's convert this property into a ``cached_property``: + +.. code-block:: python + + from cached_property import cached_property + + class MyClass(object): + + @cached_property + def my_cached_property(self): + print('Computing my_cached_property...') + return 42 + +Now when we run it the computation is performed only once: + +.. code-block:: python + + >>> my_object = MyClass() + >>> my_object.my_cached_property + Computing my_property... + 42 + >>> my_object.my_cached_property + 42 + >>> my_object.my_cached_property + 42 + +Why doesn't the value of ``my_object.my_cached_property`` change? Because it's +a **cached property**! + +Inspecting the cache +-------------------- -Let's convert the boardwalk property into a ``cached_property``. +Sometimes you may want to list all the cached properties of an object. In +order to demonstrate this, let us define a class with several cached +properties: .. code-block:: python from cached_property import cached_property - class Monopoly(object): + class MyClass(object): - def __init__(self): - self.boardwalk_price = 500 + @cached_property + def my_cached_property(self): + print('Computing my_cached_property...') + return 42 @cached_property - def boardwalk(self): - # Again, this is a silly example. Don't worry about it, this is - # just an example for clarity. - self.boardwalk_price += 50 - return self.boardwalk_price + def my_second_cached_property(self): + print('Computing my_second_cached_property...') + return 51 -Now when we run it the price stays at $550. +To list all the cached properties of an object, use the function +``cached_properties``: .. code-block:: python - >>> monopoly = Monopoly() - >>> monopoly.boardwalk - 550 - >>> monopoly.boardwalk - 550 - >>> monopoly.boardwalk - 550 + >>> from cached_property import cached_properties + >>> my_object = MyClass() + >>> for property_name in cached_properties(my_object): + ... print(property_name) + my_cached_property + my_second_cached_property + +To test which properties are already cached, use ``is_cached``: -Why doesn't the value of ``monopoly.boardwalk`` change? Because it's a **cached property**! +.. code-block:: python + + >>> from cached_property import is_cached + >>> my_object = MyClass() + >>> my_object.my_cached_property + Computing my_property... + 42 + >>> is_cached(my_object, 'my_cached_property') + True + >>> is_cached(my_object, 'my_second_cached_property') + False Invalidating the Cache ---------------------- -Results of cached functions can be invalidated by outside forces. Let's demonstrate how to force the cache to invalidate: +Results of cached functions can be invalidated by outside forces. To +demonstrate this, let's define first an object as we already did: + + >>> my_object = MyClass() + >>> my_object.my_cached_property + Computing my_cached_property... + 42 + >>> my_object.my_second_cached_property + Computing my_second_cached_property... + 51 + +To delete the cache for one property in particular, use ``un_cache``: .. code-block:: python - >>> monopoly = Monopoly() - >>> monopoly.boardwalk - 550 - >>> monopoly.boardwalk - 550 - >>> # invalidate the cache - >>> del monopoly.__dict__['boardwalk'] - >>> # request the boardwalk property again - >>> monopoly.boardwalk - 600 - >>> monopoly.boardwalk - 600 + >>> from cached_property import un_cache + >>> un_cache(my_object, 'my_cached_property') + >>> my_object.my_cached_property + Computing my_cached_property... + 42 + >>> my_object.my_second_cached_property + 51 + +To delete the cache of the whole object, use ``delete_cache``: + +.. code-block:: python + + >>> from cached_property import delete_cache + >>> delete_cache(my_object) + >>> my_object.my_cached_property + Computing my_cached_property... + 42 + >>> my_object.my_second_cached_property + Computing my_second_cached_property... + 51 + +Property deleting the cache +--------------------------- + +Sometimes, you want to define a property that automatically deletes the cache +of the object when the property is set or deleted. You can use +``property_deleting_cache``: + +.. code-block:: python + + from cached_property import cached_property, property_deleting_cache + + class MyClass(object): + + def __init__(self, my_parameter): + self.my_parameter = my_parameter + + @property_deleting_cache + def my_parameter(self): + print('Accessing my_parameter...') + + @cached_property + def my_cached_property(self): + print('Computing my_cached_property...') + return self.my_parameter + 1 + +Then use it: + +.. code-block:: python + + >>> my_object = MyClass(my_parameter=41) + >>> my_object.my_cached_property + Computing my_cached_property... + Accessing my_parameter... + 42 + >>> my_object.my_cached_property + 42 + >>> my_object.my_parameter = 50 + >>> my_object.my_cached_property + Computing my_cached_property... + Accessing my_parameter... + 51 + >>> my_object.my_cached_property + 51 Working with Threads --------------------- -What if a whole bunch of people want to stay at Boardwalk all at once? This means using threads, which -unfortunately causes problems with the standard ``cached_property``. In this case, switch to using the +What if a whole bunch of people want to access ``my_cached_property`` +all at once? This means using threads, which unfortunately causes problems +with the standard ``cached_property``. In this case, switch to using the ``threaded_cached_property``: .. code-block:: python from cached_property import threaded_cached_property - class Monopoly(object): - - def __init__(self): - self.boardwalk_price = 500 + class MyClass(object): @threaded_cached_property - def boardwalk(self): - """threaded_cached_property is really nice for when no one waits - for other people to finish their turn and rudely start rolling - dice and moving their pieces.""" - - sleep(1) - self.boardwalk_price += 50 - return self.boardwalk_price + def my_cached_property(self): + print('Computing my_cached_property...') + return 42 Now use it: .. code-block:: python >>> from threading import Thread - >>> from monopoly import Monopoly - >>> monopoly = Monopoly() + >>> my_object = MyClass() >>> threads = [] >>> for x in range(10): - >>> thread = Thread(target=lambda: monopoly.boardwalk) - >>> thread.start() - >>> threads.append(thread) + ... thread = Thread(target=lambda: my_object.my_cached_property) + ... thread.start() + ... threads.append(thread) >>> for thread in threads: - >>> thread.join() - - >>> self.assertEqual(m.boardwalk, 550) + ... thread.join() + Computing my_cached_property... +Please note that ``my_cached_property`` was computed only once, as usual. Working with async/await (Python 3.5+) -------------------------------------- @@ -160,30 +260,28 @@ computed once and then cached: from cached_property import cached_property - class Monopoly(object): - - def __init__(self): - self.boardwalk_price = 500 + class MyClass(object): @cached_property - async def boardwalk(self): - self.boardwalk_price += 50 - return self.boardwalk_price + async def my_cached_property(self): + print('Computing my_cached_property...') + return 42 Now use it: .. code-block:: python - >>> async def print_boardwalk(): - ... monopoly = Monopoly() - ... print(await monopoly.boardwalk) - ... print(await monopoly.boardwalk) - ... print(await monopoly.boardwalk) + >>> async def print_my_cached_property(): + ... my_object = MyClass() + ... print(await my_object.my_cached_property) + ... print(await my_object.my_cached_property) + ... print(await my_object.my_cached_property) >>> import asyncio - >>> asyncio.get_event_loop().run_until_complete(print_boardwalk()) - 550 - 550 - 550 + >>> asyncio.get_event_loop().run_until_complete(print_my_cached_property()) + Computing my_cached_property... + 42 + 42 + 42 Note that this does not work with threading either, most asyncio objects are not thread-safe. And if you run separate event loops in @@ -191,7 +289,6 @@ each thread, the cached version will most likely have the wrong event loop. To summarize, either use cooperative multitasking (event loop) or threading, but not both at the same time. - Timing out the cache -------------------- @@ -203,31 +300,30 @@ versions of ``cached_property`` and ``threaded_cached_property``. import random from cached_property import cached_property_with_ttl - class Monopoly(object): + class MyClass(object): - @cached_property_with_ttl(ttl=5) # cache invalidates after 5 seconds - def dice(self): - # I dare the reader to implement a game using this method of 'rolling dice'. - return random.randint(2,12) + @cached_property_with_ttl(ttl=2) # cache invalidates after 2 seconds + def my_cached_property(self): + print('Computing my_cached_property...') + return random.randint(1, 100) Now use it: .. code-block:: python - >>> monopoly = Monopoly() - >>> monopoly.dice - 10 - >>> monopoly.dice - 10 + >>> my_object = MyClass() + >>> my_object.my_cached_property + 42 + >>> my_object.my_cached_property + 42 >>> from time import sleep - >>> sleep(6) # Sleeps long enough to expire the cache - >>> monopoly.dice - 3 - >>> monopoly.dice - 3 - -**Note:** The ``ttl`` tools do not reliably allow the clearing of the cache. This -is why they are broken out into seperate tools. See https://github.com/pydanny/cached-property/issues/16. + >>> sleep(3) # Sleeps long enough to expire the cache + >>> my_object.my_cached_property + 51 + +**Note:** The ``ttl`` tools do not reliably allow the clearing of the cache. +This is why they are broken out into separate tools. See +https://github.com/pydanny/cached-property/issues/16. Credits -------- diff --git a/readme_companion.ipynb b/readme_companion.ipynb new file mode 100644 index 0000000..b80eb62 --- /dev/null +++ b/readme_companion.ipynb @@ -0,0 +1,1080 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to use it" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.028323Z", + "start_time": "2020-03-05T16:34:58.022338Z" + } + }, + "outputs": [], + "source": [ + "class MyClass(object):\n", + "\n", + " @property\n", + " def my_property(self):\n", + " # In reality, this might represent a database call or time\n", + " # intensive task like calling a third-party API.\n", + " print('Computing my_property...') \n", + " return 42" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.073202Z", + "start_time": "2020-03-05T16:34:58.030318Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object = MyClass()\n", + "my_object.my_property" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.088163Z", + "start_time": "2020-03-05T16:34:58.079186Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_property" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.116089Z", + "start_time": "2020-03-05T16:34:58.095144Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import cached_property\n", + "\n", + "class MyClass(object):\n", + "\n", + " @cached_property\n", + " def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return 42" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.130050Z", + "start_time": "2020-03-05T16:34:58.119080Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object = MyClass()\n", + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.140024Z", + "start_time": "2020-03-05T16:34:58.132046Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Inspecting the cache" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.148999Z", + "start_time": "2020-03-05T16:34:58.143016Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import cached_property, cached_properties, is_cached" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.157986Z", + "start_time": "2020-03-05T16:34:58.152990Z" + } + }, + "outputs": [], + "source": [ + "class MyClass(object):\n", + "\n", + " @cached_property\n", + " def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return 42\n", + " \n", + " @cached_property\n", + " def my_second_cached_property(self):\n", + " print('Computing my_second_cached_property...')\n", + " return 51" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.168946Z", + "start_time": "2020-03-05T16:34:58.161965Z" + } + }, + "outputs": [], + "source": [ + "my_object = MyClass()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.178919Z", + "start_time": "2020-03-05T16:34:58.170941Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "my_cached_property\n", + "my_second_cached_property\n" + ] + } + ], + "source": [ + "for property_name in cached_properties(my_object):\n", + " print(property_name)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.188893Z", + "start_time": "2020-03-05T16:34:58.180914Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.197870Z", + "start_time": "2020-03-05T16:34:58.190890Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "is_cached(my_object, 'my_cached_property')" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.206845Z", + "start_time": "2020-03-05T16:34:58.199864Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "is_cached(my_object, 'my_second_cached_property')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Invalidating the cache" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.215823Z", + "start_time": "2020-03-05T16:34:58.208839Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import cached_property, un_cache, delete_cache" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.224797Z", + "start_time": "2020-03-05T16:34:58.218813Z" + } + }, + "outputs": [], + "source": [ + "class MyClass(object):\n", + "\n", + " @cached_property\n", + " def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return 42\n", + " \n", + " @cached_property\n", + " def my_second_cached_property(self):\n", + " print('Computing my_second_cached_property...')\n", + " return 51" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.233773Z", + "start_time": "2020-03-05T16:34:58.226792Z" + } + }, + "outputs": [], + "source": [ + "my_object = MyClass()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.243746Z", + "start_time": "2020-03-05T16:34:58.234770Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.252723Z", + "start_time": "2020-03-05T16:34:58.245741Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_second_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "51" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_second_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.260701Z", + "start_time": "2020-03-05T16:34:58.253721Z" + } + }, + "outputs": [], + "source": [ + "un_cache(my_object, 'my_cached_property')" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.270674Z", + "start_time": "2020-03-05T16:34:58.262696Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.278653Z", + "start_time": "2020-03-05T16:34:58.271674Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "51" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_second_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.286642Z", + "start_time": "2020-03-05T16:34:58.279650Z" + } + }, + "outputs": [], + "source": [ + "delete_cache(my_object)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.299597Z", + "start_time": "2020-03-05T16:34:58.290621Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.307576Z", + "start_time": "2020-03-05T16:34:58.301592Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_second_cached_property...\n" + ] + }, + { + "data": { + "text/plain": [ + "51" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_second_cached_property" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Property deleting the cache" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.314557Z", + "start_time": "2020-03-05T16:34:58.309570Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import cached_property, property_deleting_cache" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.322536Z", + "start_time": "2020-03-05T16:34:58.316552Z" + } + }, + "outputs": [], + "source": [ + "class MyClass(object):\n", + "\n", + " def __init__(self, my_parameter):\n", + " self.my_parameter = my_parameter\n", + " \n", + " @property_deleting_cache\n", + " def my_parameter(self):\n", + " \"A parameter that deletes the cache when set or deleted.\"\n", + " print('Accessing my_parameter...')\n", + " \n", + " @cached_property\n", + " def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return self.my_parameter + 1" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.334504Z", + "start_time": "2020-03-05T16:34:58.323533Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n", + "Accessing my_parameter...\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object = MyClass(my_parameter=41)\n", + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.342483Z", + "start_time": "2020-03-05T16:34:58.335501Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.351459Z", + "start_time": "2020-03-05T16:34:58.344478Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n", + "Accessing my_parameter...\n" + ] + }, + { + "data": { + "text/plain": [ + "51" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_parameter = 50\n", + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.361432Z", + "start_time": "2020-03-05T16:34:58.352455Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "51" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Working with Threads" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.368413Z", + "start_time": "2020-03-05T16:34:58.363428Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import threaded_cached_property\n", + "\n", + "class MyClass(object):\n", + "\n", + " @threaded_cached_property\n", + " def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return 42" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.383374Z", + "start_time": "2020-03-05T16:34:58.369411Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n" + ] + } + ], + "source": [ + "from threading import Thread\n", + "my_object = MyClass()\n", + "threads = []\n", + "for x in range(10):\n", + " thread = Thread(target=lambda: my_object.my_cached_property)\n", + " thread.start()\n", + " threads.append(thread)\n", + "\n", + "for thread in threads:\n", + " thread.join()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Working with async/await (Python 3.5+)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.394345Z", + "start_time": "2020-03-05T16:34:58.385370Z" + } + }, + "outputs": [], + "source": [ + "# This is just a trick to make asyncio work in jupyter.\n", + "# Cf. https://markhneedham.com/blog/2019/05/10/jupyter-runtimeerror-this-event-loop-is-already-running/\n", + "import nest_asyncio\n", + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.402323Z", + "start_time": "2020-03-05T16:34:58.396339Z" + } + }, + "outputs": [], + "source": [ + "from cached_property import cached_property\n", + "\n", + "class MyClass(object):\n", + "\n", + " @cached_property\n", + " async def my_cached_property(self):\n", + " print('Computing my_cached_property...')\n", + " return 42" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.413302Z", + "start_time": "2020-03-05T16:34:58.404317Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing my_cached_property...\n", + "42\n", + "42\n", + "42\n" + ] + } + ], + "source": [ + "async def print_my_cached_property():\n", + " my_object = MyClass()\n", + " print(await my_object.my_cached_property)\n", + " print(await my_object.my_cached_property)\n", + " print(await my_object.my_cached_property)\n", + "import asyncio\n", + "asyncio.get_event_loop().run_until_complete(print_my_cached_property())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Timing out the cache" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.420276Z", + "start_time": "2020-03-05T16:34:58.415287Z" + } + }, + "outputs": [], + "source": [ + "import random\n", + "from cached_property import cached_property_with_ttl\n", + "\n", + "class MyClass(object):\n", + "\n", + " @cached_property_with_ttl(ttl=2) # cache invalidates after 2 seconds\n", + " def my_cached_property(self):\n", + " return random.randint(1, 100)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.430248Z", + "start_time": "2020-03-05T16:34:58.422269Z" + } + }, + "outputs": [], + "source": [ + "my_object = MyClass()" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.439223Z", + "start_time": "2020-03-05T16:34:58.431245Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "66" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:34:58.448200Z", + "start_time": "2020-03-05T16:34:58.441218Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "66" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:35:01.458856Z", + "start_time": "2020-03-05T16:34:58.450195Z" + } + }, + "outputs": [], + "source": [ + "from time import sleep\n", + "sleep(3) # Sleeps long enough to expire the cache" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "ExecuteTime": { + "end_time": "2020-03-05T16:35:01.485791Z", + "start_time": "2020-03-05T16:35:01.466838Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "45" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_object.my_cached_property" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": false, + "sideBar": true, + "skip_h1_title": true, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}