{ "cells": [ { "cell_type": "markdown", "id": "f6c1183f", "metadata": {}, "source": [ "# Cached methods/attributes\n", ":::{eval-rst}\n", ".. currentmodule:: module_utilities\n", ":::\n", "\n", "It is common to want to cache class methods and attributes. {mod}`~module_utilities.cached` provides a very simple way to do that. Note that advanced features like LRU caching, etc, are not supported yet.\n", "\n", "The main idea is to replace boiler plate like this:" ] }, { "cell_type": "code", "execution_count": 21, "id": "fb0110c0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "setting xlist\n", "value of xlist: [2, 4]\n", "value of xlist: [2, 4]\n" ] } ], "source": [ "class Example_boiler:\n", " def __init__(self, x):\n", " self._x = x\n", "\n", " @property\n", " def xlist(self):\n", " if not hasattr(self, \"_xlist\"):\n", " print(\"setting xlist\")\n", " self._xlist = [self._x, self._x**2]\n", " return self._xlist\n", "\n", "\n", "e = Example_boiler(x=2)\n", "print(\"value of xlist:\", e.xlist)\n", "print(\"value of xlist:\", e.xlist)" ] }, { "cell_type": "markdown", "id": "40e95f1a", "metadata": {}, "source": [ "While this works perfectly well, it has two drawbacks:\n", "\n", "1. lots of boiler plate\n", "2. Ugly if you want to clear out the cached value." ] }, { "cell_type": "markdown", "id": "27a80ece", "metadata": {}, "source": [ "## Cached property\n", "\n", "First, lets look at a caching a property. For this we'll use the {func}`cached.prop` decorator:" ] }, { "cell_type": "code", "execution_count": 22, "id": "7d0ff1d6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "setting xlist\n", "value of xlist: [2, 4]\n", "value of xlist: [2, 4]\n" ] } ], "source": [ "from module_utilities import cached\n", "\n", "\n", "class Example_cached:\n", " def __init__(self, x):\n", " self._x = x\n", "\n", " @cached.prop\n", " def xlist(self):\n", " print(\"setting xlist\")\n", " return [self._x, self._x**2]\n", "\n", "\n", "e = Example_cached(x=2)\n", "print(\"value of xlist:\", e.xlist)\n", "print(\"value of xlist:\", e.xlist)" ] }, { "cell_type": "markdown", "id": "060ec72c", "metadata": {}, "source": [ "In short, the value is cached to a dictionary `self._cache`. This dictionary is created if it doesn't alread exist.\n", "Note that if using `__slots__`, you'll need to include `_cache`. Looking at our example, we see that:" ] }, { "cell_type": "code", "execution_count": 23, "id": "ff6d6ee4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'xlist': [2, 4]}" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "e._cache" ] }, { "cell_type": "markdown", "id": "bc6eb453", "metadata": {}, "source": [ "## Cached method\n", "\n", "We can also cache methods using {func}`cached.meth`:" ] }, { "cell_type": "code", "execution_count": 34, "id": "1873c223", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "getting ylist\n", "setting xlist\n", "value of yzlist(3, 4) : [2, 4, 3, 4]\n", "value of yzlist(3) : [2, 4, 3, 4]\n", "value of yzlist(z=4, y=3): [2, 4, 3, 4]\n", "getting ylist\n", "new value [2, 4, 'y', 'z']\n" ] } ], "source": [ "class Example_with_method(Example_cached):\n", " @cached.meth\n", " def yzlist(self, y, z=4):\n", " print(\"getting ylist\")\n", " return self.xlist + [y, z]\n", "\n", "\n", "e = Example_with_method(x=2)\n", "print(\"value of yzlist(3, 4) :\", e.yzlist(3, 4))\n", "# respects default values\n", "print(\"value of yzlist(3) :\", e.yzlist(3))\n", "# respects named arguments\n", "print(\"value of yzlist(z=4, y=3):\", e.yzlist(z=4, y=3))\n", "\n", "print(\"new value\", e.yzlist(\"y\", \"z\"))" ] }, { "cell_type": "markdown", "id": "b2ecdcbc", "metadata": {}, "source": [ "This results in the cache:" ] }, { "cell_type": "code", "execution_count": 37, "id": "2fab4748", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'xlist': [2, 4],\n", " ('yzlist', (3, 4), frozenset()): [2, 4, 3, 4],\n", " ('yzlist', ('y', 'z'), frozenset()): [2, 4, 'y', 'z']}" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "e._cache" ] }, { "cell_type": "markdown", "id": "dbcb2bc8", "metadata": {}, "source": [ "{func}`cached.meth` also works with arbitrary `*args` and `**kwargs`:" ] }, { "cell_type": "code", "execution_count": 54, "id": "ececcbbd", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "getting example\n", "{'y': 1, 'args': (3,), '**kwargs': {'a': 'a'}}\n", "{'y': 1, 'args': (3,), '**kwargs': {'a': 'a'}}\n" ] } ], "source": [ "class Example_with_methd2(Example_cached):\n", " @cached.meth\n", " def example(self, y=1, z=2, *args, **kwargs):\n", " print(\"getting example\")\n", " return {\"y\": y, \"args\": args, \"**kwargs\": kwargs}\n", "\n", "\n", "e = Example_with_methd2(x=2)\n", "\n", "print(e.example(1, 2, 3, a=\"a\"))\n", "print(e.example(1, 2, 3, a=\"a\"))" ] }, { "cell_type": "code", "execution_count": 55, "id": "49322119", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{('example', (1, 2, 3), frozenset({('a', 'a')})): {'y': 1,\n", " 'args': (3,),\n", " '**kwargs': {'a': 'a'}}}" ] }, "execution_count": 55, "metadata": {}, "output_type": "execute_result" } ], "source": [ "e._cache" ] }, { "cell_type": "markdown", "id": "4508d939", "metadata": {}, "source": [ "Note that isn't perfect though. If you mix what is an arg and what is a kwargs, it will give a different cache:" ] }, { "cell_type": "code", "execution_count": 61, "id": "67f16da2", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "getting example\n", "{'args': (1, 2), 'kwargs': {'x': 'x', 'y': 'y'}}\n", "getting example\n", "{'args': (1, 2, 'x'), 'kwargs': {'y': 'y'}}\n" ] } ], "source": [ "class Example:\n", " @cached.meth\n", " def example(self, *args, **kwargs):\n", " print(\"getting example\")\n", " return {\"args\": args, \"kwargs\": kwargs}\n", "\n", "\n", "e = Example()\n", "\n", "print(e.example(1, 2, x=\"x\", y=\"y\"))\n", "print(e.example(1, 2, \"x\", y=\"y\"))" ] }, { "cell_type": "markdown", "id": "ddc87daa", "metadata": {}, "source": [ "So use with caution" ] }, { "cell_type": "markdown", "id": "e6658bd9", "metadata": {}, "source": [ "## Clearing cache:" ] }, { "cell_type": "markdown", "id": "b64f1b25", "metadata": {}, "source": [ "First, note that the key in `_cache` is defaults to the name of the function. You can override this by setting `key={value}` when calling the decorator:" ] }, { "cell_type": "code", "execution_count": 57, "id": "79eed35d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "setting aprop\n", "hello\n", "{'myprop': 'hello'}\n" ] } ], "source": [ "class Example:\n", " @cached.prop(key=\"myprop\")\n", " def aprop(self):\n", " print(\"setting aprop\")\n", " return \"hello\"\n", "\n", "\n", "x = Example()\n", "print(x.aprop)\n", "print(x._cache)" ] }, { "cell_type": "markdown", "id": "c8bb8aca", "metadata": {}, "source": [ "Now, what if you want to clear out the cache? For example, if some class variable is changed? For this, use {func}`cached.clear`" ] }, { "cell_type": "code", "execution_count": 83, "id": "8d394a00", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "first call:\n", "setting aprop\n", "aprop 4\n", "setting bprop\n", "bprop 8\n", "setting meth\n", "meth(1) 3\n", "setting meth\n", "meth(2) 4\n", "\n", "second call:\n", "aprop 4\n", "bprop 8\n", "meth(1) 3\n", "meth(2) 4\n" ] } ], "source": [ "class Example_clear:\n", " def __init__(self, a):\n", " self._a = a\n", "\n", " @property\n", " def a(self):\n", " return self._a\n", "\n", " @a.setter\n", " @cached.clear\n", " def a(self, val):\n", " print(\"clear all from a\")\n", " self._a = val\n", "\n", " @cached.prop\n", " def aprop(self):\n", " print(\"setting aprop\")\n", " return self.a**2\n", "\n", " @cached.prop(key=\"myprop\")\n", " def bprop(self):\n", " print(\"setting bprop\")\n", " return self.a**3\n", "\n", " @cached.meth\n", " def meth(self, x):\n", " print(\"setting meth\")\n", " return self.a + x\n", "\n", " @cached.clear(\"myprop\")\n", " def meth_that_clears_myprop(self):\n", " pass\n", "\n", " @cached.clear(\"meth\")\n", " def meth_that_clears_meth(self):\n", " pass\n", "\n", "\n", "def print_vals(e):\n", " print(\"aprop \", e.aprop)\n", " print(\"bprop \", e.bprop)\n", " print(\"meth(1)\", e.meth(x=1))\n", " print(\"meth(2)\", e.meth(x=2))\n", "\n", "\n", "e = Example_clear(a=2)\n", "\n", "print(\"\\nfirst call:\")\n", "print_vals(e)\n", "\n", "print(\"\\nsecond call:\")\n", "print_vals(e)" ] }, { "cell_type": "code", "execution_count": 84, "id": "e1729412", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "clear all from a\n", "{}\n", "call again:\n", "setting aprop\n", "aprop 4\n", "setting bprop\n", "bprop 8\n", "setting meth\n", "meth(1) 3\n", "setting meth\n", "meth(2) 4\n" ] } ], "source": [ "# reset a value\n", "e.a = 2\n", "print(e._cache)\n", "\n", "print(\"call again:\")\n", "print_vals(e)" ] }, { "cell_type": "code", "execution_count": 85, "id": "953c24bd", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'aprop': 4, 'myprop': 8, ('meth', (1,), frozenset()): 3, ('meth', (2,), frozenset()): 4}\n", "{'aprop': 4, ('meth', (1,), frozenset()): 3, ('meth', (2,), frozenset()): 4}\n", "aprop 4\n", "setting bprop\n", "bprop 8\n", "meth(1) 3\n", "meth(2) 4\n" ] } ], "source": [ "# clear a single method:\n", "print(e._cache)\n", "e.meth_that_clears_myprop()\n", "print(e._cache)\n", "print_vals(e)" ] }, { "cell_type": "code", "execution_count": 86, "id": "a12fca3b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'aprop': 4, ('meth', (1,), frozenset()): 3, ('meth', (2,), frozenset()): 4, 'myprop': 8}\n", "{'aprop': 4, 'myprop': 8}\n", "aprop 4\n", "bprop 8\n", "setting meth\n", "meth(1) 3\n", "setting meth\n", "meth(2) 4\n" ] } ], "source": [ "# clearing a method clears all calls to method key\n", "print(e._cache)\n", "e.meth_that_clears_meth()\n", "print(e._cache)\n", "print_vals(e)" ] } ], "metadata": { "kernelspec": { "display_name": "module-utilities-dev [conda env:dev-3]", "language": "python", "name": "conda-env-dev-3-py" }, "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.10.10" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "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": 5 }