Skip to content

Commit 33f4f0f

Browse files
committed
fixes #580
1 parent d2bd7e5 commit 33f4f0f

File tree

3 files changed

+110
-12
lines changed

3 files changed

+110
-12
lines changed

fastcore/_modidx.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,7 @@
660660
'fastcore.xtras.shufflish': ('xtras.html#shufflish', 'fastcore/xtras.py'),
661661
'fastcore.xtras.sparkline': ('xtras.html#sparkline', 'fastcore/xtras.py'),
662662
'fastcore.xtras.stringfmt_names': ('xtras.html#stringfmt_names', 'fastcore/xtras.py'),
663+
'fastcore.xtras.timed_cache': ('xtras.html#timed_cache', 'fastcore/xtras.py'),
663664
'fastcore.xtras.trace': ('xtras.html#trace', 'fastcore/xtras.py'),
664665
'fastcore.xtras.truncstr': ('xtras.html#truncstr', 'fastcore/xtras.py'),
665666
'fastcore.xtras.type2str': ('xtras.html#type2str', 'fastcore/xtras.py'),

fastcore/xtras.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
'ReindexCollection', 'get_source_link', 'truncstr', 'sparkline', 'modify_exception', 'round_multiple',
1111
'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', 'stringfmt_names', 'PartialFormatter',
1212
'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', 'ContextManagers', 'shufflish',
13-
'console_help', 'hl_md', 'type2str', 'dataclass_src', 'nullable_dc', 'make_nullable', 'mk_dataclass']
13+
'console_help', 'hl_md', 'type2str', 'dataclass_src', 'nullable_dc', 'make_nullable', 'mk_dataclass',
14+
'timed_cache']
1415

1516
# %% ../nbs/03_xtras.ipynb 2
1617
from .imports import *
@@ -21,6 +22,7 @@
2122
import string,time
2223
from contextlib import contextmanager,ExitStack
2324
from datetime import datetime, timezone
25+
from time import sleep,time,perf_counter
2426

2527
# %% ../nbs/03_xtras.ipynb 7
2628
def walk(
@@ -504,7 +506,7 @@ class EventTimer:
504506

505507
def __init__(self, store=5, span=60):
506508
import collections
507-
self.hist,self.span,self.last = collections.deque(maxlen=store),span,time.perf_counter()
509+
self.hist,self.span,self.last = collections.deque(maxlen=store),span,perf_counter()
508510
self._reset()
509511

510512
def _reset(self): self.start,self.events = self.last,0
@@ -515,10 +517,10 @@ def add(self, n=1):
515517
self.hist.append(self.freq)
516518
self._reset()
517519
self.events +=n
518-
self.last = time.perf_counter()
520+
self.last = perf_counter()
519521

520522
@property
521-
def duration(self): return time.perf_counter()-self.start
523+
def duration(self): return perf_counter()-self.start
522524
@property
523525
def freq(self): return self.events/self.duration
524526

@@ -682,3 +684,25 @@ def mk_dataclass(cls):
682684
if not hasattr(cls,k) or getattr(cls,k) is MISSING:
683685
setattr(cls, k, field(default=None))
684686
dataclass(cls, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
687+
688+
# %% ../nbs/03_xtras.ipynb 180
689+
def timed_cache(seconds=60, maxsize=128):
690+
"Like `lru_cache`, but also with time-based eviction"
691+
def decorator(func):
692+
cache = {}
693+
@wraps(func)
694+
def wrapper(*args, **kwargs):
695+
key = str(args) + str(kwargs)
696+
now = time()
697+
if key in cache:
698+
result, timestamp = cache[key]
699+
if seconds == 0 or now - timestamp < seconds:
700+
cache[key] = cache.pop(key)
701+
return result
702+
del cache[key]
703+
result = func(*args, **kwargs)
704+
cache[key] = (result, now)
705+
if len(cache) > maxsize: cache.pop(next(iter(cache)))
706+
return result
707+
return wrapper
708+
return decorator

nbs/03_xtras.ipynb

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"from functools import wraps\n",
3434
"import string,time\n",
3535
"from contextlib import contextmanager,ExitStack\n",
36-
"from datetime import datetime, timezone"
36+
"from datetime import datetime, timezone\n",
37+
"from time import sleep,time,perf_counter"
3738
]
3839
},
3940
{
@@ -47,7 +48,6 @@
4748
"from nbdev.showdoc import *\n",
4849
"from fastcore.nb_imports import *\n",
4950
"\n",
50-
"from time import sleep\n",
5151
"import shutil,tempfile,pickle,random\n",
5252
"from dataclasses import dataclass"
5353
]
@@ -1727,7 +1727,7 @@
17271727
{
17281728
"data": {
17291729
"text/plain": [
1730-
"['h', 'g', 'a', 'd', 'b', 'e', 'f', 'c']"
1730+
"['a', 'h', 'f', 'b', 'c', 'g', 'e', 'd']"
17311731
]
17321732
},
17331733
"execution_count": null,
@@ -2114,7 +2114,7 @@
21142114
"\n",
21152115
" def __init__(self, store=5, span=60):\n",
21162116
" import collections\n",
2117-
" self.hist,self.span,self.last = collections.deque(maxlen=store),span,time.perf_counter()\n",
2117+
" self.hist,self.span,self.last = collections.deque(maxlen=store),span,perf_counter()\n",
21182118
" self._reset()\n",
21192119
"\n",
21202120
" def _reset(self): self.start,self.events = self.last,0\n",
@@ -2125,10 +2125,10 @@
21252125
" self.hist.append(self.freq)\n",
21262126
" self._reset()\n",
21272127
" self.events +=n\n",
2128-
" self.last = time.perf_counter()\n",
2128+
" self.last = perf_counter()\n",
21292129
"\n",
21302130
" @property\n",
2131-
" def duration(self): return time.perf_counter()-self.start\n",
2131+
" def duration(self): return perf_counter()-self.start\n",
21322132
" @property\n",
21332133
" def freq(self): return self.events/self.duration"
21342134
]
@@ -2188,8 +2188,8 @@
21882188
"name": "stdout",
21892189
"output_type": "stream",
21902190
"text": [
2191-
"Num Events: 3, Freq/sec: 256.2\n",
2192-
"Most recent: ▁▃▇▅▂ 223.9 260.6 307.1 279.5 252.0\n"
2191+
"Num Events: 3, Freq/sec: 316.2\n",
2192+
"Most recent: ▇▁▂▃▁ 288.7 227.7 246.5 256.5 217.9\n"
21932193
]
21942194
}
21952195
],
@@ -2839,6 +2839,79 @@
28392839
"Person(name=\"Bob\")"
28402840
]
28412841
},
2842+
{
2843+
"cell_type": "code",
2844+
"execution_count": null,
2845+
"metadata": {},
2846+
"outputs": [],
2847+
"source": [
2848+
"from functools import wraps\n",
2849+
"from time import time"
2850+
]
2851+
},
2852+
{
2853+
"cell_type": "code",
2854+
"execution_count": null,
2855+
"metadata": {},
2856+
"outputs": [],
2857+
"source": [
2858+
"#| export\n",
2859+
"def timed_cache(seconds=60, maxsize=128):\n",
2860+
" \"Like `lru_cache`, but also with time-based eviction\"\n",
2861+
" def decorator(func):\n",
2862+
" cache = {}\n",
2863+
" @wraps(func)\n",
2864+
" def wrapper(*args, **kwargs):\n",
2865+
" key = str(args) + str(kwargs)\n",
2866+
" now = time()\n",
2867+
" if key in cache:\n",
2868+
" result, timestamp = cache[key]\n",
2869+
" if seconds == 0 or now - timestamp < seconds:\n",
2870+
" cache[key] = cache.pop(key)\n",
2871+
" return result\n",
2872+
" del cache[key]\n",
2873+
" result = func(*args, **kwargs)\n",
2874+
" cache[key] = (result, now)\n",
2875+
" if len(cache) > maxsize: cache.pop(next(iter(cache)))\n",
2876+
" return result\n",
2877+
" return wrapper\n",
2878+
" return decorator"
2879+
]
2880+
},
2881+
{
2882+
"cell_type": "code",
2883+
"execution_count": null,
2884+
"metadata": {},
2885+
"outputs": [],
2886+
"source": [
2887+
"@timed_cache(seconds=0.1, maxsize=2)\n",
2888+
"def cached_func(x): return x * 2, time()\n",
2889+
"\n",
2890+
"# basic caching\n",
2891+
"result1, time1 = cached_func(2)\n",
2892+
"test_eq(result1, 4)\n",
2893+
"sleep(0.01)\n",
2894+
"result2, time2 = cached_func(2)\n",
2895+
"test_eq(result2, 4)\n",
2896+
"test_eq(time1, time2)\n",
2897+
"\n",
2898+
"# caching different values\n",
2899+
"result3, _ = cached_func(3)\n",
2900+
"test_eq(result3, 6)\n",
2901+
"\n",
2902+
"# maxsize\n",
2903+
"_, time4 = cached_func(4)\n",
2904+
"_, time2_new = cached_func(2)\n",
2905+
"test_close(time2, time2_new, eps=0.1)\n",
2906+
"_, time3_new = cached_func(3)\n",
2907+
"test_ne(time3_new, time())\n",
2908+
"\n",
2909+
"# time expiration\n",
2910+
"sleep(0.2)\n",
2911+
"_, time4_new = cached_func(4)\n",
2912+
"test_ne(time4_new, time())"
2913+
]
2914+
},
28422915
{
28432916
"cell_type": "markdown",
28442917
"metadata": {},

0 commit comments

Comments
 (0)