{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "0", "metadata": { "tags": [ "remove-cell" ] }, "outputs": [], "source": [ "# mypy: disable-error-code=\"no-untyped-def, no-untyped-call, assignment, arg-type\"\n", "# pyright: reportUnusedExpression=false\n", "from __future__ import annotations\n", "\n", "from typing import Any\n", "\n", "e: Any" ] }, { "cell_type": "markdown", "id": "1", "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": 2, "id": "2", "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 ExampleBoiler:\n", " def __init__(self, x) -> None:\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 = ExampleBoiler(x=2)\n", "print(\"value of xlist:\", e.xlist)\n", "print(\"value of xlist:\", e.xlist)" ] }, { "cell_type": "markdown", "id": "3", "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": "4", "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": 3, "id": "5", "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 typing import Any\n", "\n", "from module_utilities import cached\n", "\n", "\n", "class ExampleCached:\n", " def __init__(self, x) -> None:\n", " self._x = x\n", " # This is not needed, but\n", " # if you use typecheckers like mypy,\n", " # it's needed\n", " self._cache: dict[str, Any] = {}\n", "\n", " @cached.prop\n", " def xlist(self):\n", " print(\"setting xlist\")\n", " return [self._x, self._x**2]\n", "\n", "\n", "e = ExampleCached(x=2)\n", "print(\"value of xlist:\", e.xlist)\n", "print(\"value of xlist:\", e.xlist)" ] }, { "cell_type": "markdown", "id": "6", "metadata": {}, "source": [ "In short, the value is cached to a dictionary `self._cache`. This dictionary is created if it doesn't already 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": 4, "id": "7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'xlist': [2, 4]}\n" ] } ], "source": [ "print(e._cache)" ] }, { "cell_type": "markdown", "id": "8", "metadata": {}, "source": [ "## Cached method\n", "\n", "We can also cache methods using {func}`.cached.meth`:" ] }, { "cell_type": "code", "execution_count": 5, "id": "9", "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 ExampleWithMethod(ExampleCached):\n", " @cached.meth\n", " def yzlist(self, y, z=4):\n", " print(\"getting ylist\")\n", " return [*self.xlist, y, z]\n", "\n", "\n", "e = ExampleWithMethod(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": "10", "metadata": {}, "source": [ "This results in the cache:" ] }, { "cell_type": "code", "execution_count": 6, "id": "11", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'yzlist': {((3, 4), frozenset()): [2, 4, 3, 4], (('y', 'z'), frozenset()): [2, 4, 'y', 'z']}, 'xlist': [2, 4]}\n" ] } ], "source": [ "print(e._cache)" ] }, { "cell_type": "markdown", "id": "12", "metadata": {}, "source": [ "{func}`.cached.meth` also works with arbitrary `*args` and `**kwargs`:" ] }, { "cell_type": "code", "execution_count": 7, "id": "13", "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 ExampleWithMethod2(ExampleCached):\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 = ExampleWithMethod2(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": 8, "id": "14", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'example': {((1, 2, 3), frozenset({('a', 'a')})): {'y': 1, 'args': (3,), '**kwargs': {'a': 'a'}}}}\n" ] } ], "source": [ "print(e._cache)" ] }, { "cell_type": "markdown", "id": "15", "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": 9, "id": "16", "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", " def __init__(self) -> None:\n", " self._cache: dict[str, Any] = {}\n", "\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": "17", "metadata": {}, "source": [ "So use with caution" ] }, { "cell_type": "markdown", "id": "18", "metadata": {}, "source": [ "## Clearing cache:" ] }, { "cell_type": "markdown", "id": "19", "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": 10, "id": "20", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "setting aprop\n", "hello\n", "{'myprop': 'hello'}\n" ] } ], "source": [ "class Example2:\n", " def __init__(self) -> None:\n", " self._cache: dict[str, Any] = {}\n", "\n", " @cached.prop(key=\"myprop\")\n", " def aprop(self) -> str:\n", " print(\"setting aprop\")\n", " return \"hello\"\n", "\n", "\n", "x = Example2()\n", "print(x.aprop)\n", "print(x._cache)" ] }, { "cell_type": "markdown", "id": "21", "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": 11, "id": "22", "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 ExampleClear:\n", " def __init__(self, a) -> None:\n", " self._a = a\n", " self._cache: dict[str, Any] = {}\n", "\n", " @property\n", " def a(self):\n", " return self._a\n", "\n", " @a.setter\n", " @cached.clear\n", " def a(self, val) -> None:\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) -> None:\n", " pass\n", "\n", " @cached.clear(\"meth\")\n", " def meth_that_clears_meth(self) -> None:\n", " pass\n", "\n", "\n", "def print_vals(e) -> None:\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 = ExampleClear(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": 12, "id": "23", "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": 13, "id": "24", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'aprop': 4, 'myprop': 8, 'meth': {((1,), frozenset()): 3, ((2,), frozenset()): 4}}\n", "{'aprop': 4, 'meth': {((1,), frozenset()): 3, ((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": 14, "id": "25", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'aprop': 4, 'meth': {((1,), frozenset()): 3, ((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": { "celltoolbar": "Tags", "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.13.4" }, "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 }