1#!/usr/bin/env python3 2""" 3# Created: Wed Aug 2 11:30:15 2023 (-0400) 4# @author: Jacob Faibussowitsch 5""" 6from __future__ import annotations 7 8from .._typing import * 9 10_T = TypeVar('_T') 11 12class AttributeCache: 13 r""" 14 An attribute cache (i.e. a `dict`) to enable lazy-evaluated default values. See the `_get_cached()` 15 method for a description and further motivation for the existence of this class. 16 """ 17 __slots__ = ('_cache',) 18 19 _cache: dict[str, Any] 20 21 def __init__(self, init_cache: Optional[dict[str, Any]] = None) -> None: 22 r"""Construct an `AttributeCache` 23 24 Parameters 25 ---------- 26 init_cache : optional 27 an existing dict to seed the cache with, or none to start fresh 28 29 Raises 30 ------ 31 TypeError 32 if `init_cache` is not None but not a `dict` 33 """ 34 if init_cache is None: 35 init_cache = {} 36 elif not isinstance(init_cache, dict): 37 raise TypeError(type(init_cache)) 38 self._cache = init_cache 39 return 40 41 def _get_cached(self, attr: str, func: Callable[..., _T], *args, **kwargs) -> _T: 42 r"""Get a cached value 43 44 Parameters 45 ---------- 46 attr : 47 the attribute name to retrieve 48 func : 49 a function to produce a default value in case `attr` is not in the cache 50 *args : 51 positional arguments to `func`, if any 52 **kwargs : 53 keyword arguments to `func`, if any 54 55 Returns 56 ------- 57 ret : 58 the value 59 60 Notes 61 ----- 62 If `attr` does not exist in the cache, this routine inserts the result of `func(*args, **kwargs)` 63 into the cache. 64 65 If all this sounds very similar to `dict.setdefault()`, then that's because it is. Normally you 66 would have some `dict` member and use `dict.setdefault()` to get a default value from it. 67 68 But this has one glaring weakness. `dict.setdefault()` (being a function call) obviously needs to 69 evaluate its arguments, i.e. the default value is computed whether or not the key already exists in 70 the dict (i.e. cache). This is OK if your default value is trivial, but what if producing it is 71 expensive? 72 73 This is why this class exists. Instead of taking the default by value, it takes the function which 74 computes it by parts, and only calls it if needed. 75 """ 76 cache = self._cache 77 if attr in cache: 78 return TYPE_CAST(_T, cache[attr]) 79 ret = cache[attr] = func(*args, **kwargs) 80 return ret 81