It’s situational. try…except is fine if you expect not to hit an exception most of the time (for some fuzzy definition of “most”).
In your case I think you may want to try something like this:
but you’re right, you’re probably storing a lot more values than needed.
Glass houses. I once spent like a whole month creating and fine tuning a very inefficient module that had the same exact purpose as the functools.cache
decorator. I didn’t even know it existed at the time 
here it is!
Take a moment to appreciate the irony that I actually imported something else from functools
.
#dynamic_programming.py
from functools import wraps
from inspect import signature, Signature, Parameter
from collections import namedtuple
class Dynamic_Memory():
__nullref = type('CustomNullPointer',(),
{'__str__' : lambda self: '<NULL>',
'__repr__': lambda self: '<NULL>',
'__bool__': lambda self: False})()
# Alternatively:
# __nullref = None
# This has the disadvantage of not recognizing 'None' as the result of calculation
__ParameterInfo = namedtuple("ParameterAsociationsWithCache", ['parameter', 'container_type', 'offset'])
__MemoryStats = namedtuple("CacheInfoStats", ['containers', 'null_pointers', 'stored_values', 'depth'])
@classmethod
def null_pointer(cls):
return cls.__nullref
############################
## basic methods ##
############################
def __init__(self, function, *, container_type = list, offset = None, init_memory = None, custodian = None):
self.__function = function
self.__calls = 0
self.__custodian = custodian
self.__sig = signature(function)
params = self.__sig.parameters
self.__container_dimension = len(params)
try:
assert self.__container_dimension > 0
for param in params.values():
assert Parameter.VAR_POSITIONAL is not param.kind is not Parameter.VAR_KEYWORD
except AssertionError:
raise ValueError("Dynamic_Memory class requires {}() to have a fixed non-zero number of arguments.".format(function.__qualname__)) from None
try:
assert self.__container_dimension == len(container_type)
except TypeError:
container_type = (container_type,)*self.__container_dimension
except AssertionError:
raise ValueError("Dynamic_Memory class requires container_type argument to be either a container class or a sequence of container classes whose len matches the number of arguments of {}().".format(function.__qualname__)) from None
self.__container = init_memory if init_memory else container_type[0]()
try:
assert self.__container_dimension == len(offset)
except TypeError:
offset = (offset,)*self.__container_dimension
except AssertionError:
raise ValueError("Dynamic_Memory class requires offset argument to be either a numeric type or a sequence of numeric types whose len matches the number of arguments of {}().".format(function.__qualname__)) from None
self.__zipped_info = [self.__ParameterInfo(p,c,o) for p,c,o in zip(params, container_type, offset)]
def __del__(self):
# Probably redundant
del self.__container
############################
## emulation methods ##
############################
def __call__ (*args, **kwargs):
self, args = args[0], args[1:]
self.__calls += 1
result, this_container, pos = self.__access_memory(args, kwargs)
if result is self.__nullref:
result = this_container[pos] = self.__function (*args, **kwargs)
return result
def __getitem__(self, key):
return self.__access_memory_direct(*self.__subscript_to_arguments(key))
def __setitem__ (self, key, value):
args, kwargs = self.__subscript_to_arguments(key)
in_cache, container, pos = self.__access_memory(args,kwargs)
if in_cache is not self.__nullref:
raise ValueError("{}() has already calculated the value at {} {}.".format(self.__function.__qualname__,args, kwargs))
else:
container[pos]= value
def __contains__(self, key):
return self.__access_memory_direct(*self.__subscript_to_arguments(key), return_bool = True)
def __str__(self):
name = str(self.__function)
if len(name)>0 and name[0]=='<' and name[-1]=='>':
name = name[1:-1]
return "<{}, with dynamic memory>".format(name)
def __repr__(self):
name = repr(self.__function)
if len(name)>0 and name[0]=='<' and name[-1]=='>':
name = name[1:-1]
return "<{}, with dynamic memory>".format(name)
def __getattr__(self, name):
return self.__function.__getattribute__(name)
def __dir__(self):
prefix = "_Dynamic_Memory"
plen = len (prefix)
return [name for name in set(super().__dir__()).union(dir(self.__function)) if prefix != name[:plen]]
############################
## public methods ##
############################
def __reset_cache (self):
self.__container = self.__zipped_info[0].container_type()
cache = property(lambda self: self.__container, None, __reset_cache, "Dynamically stored memory.")
function = property (lambda self: self.__function, None, None, "Original function")
dimensions = property (lambda self: self.__container_dimension, None, None, "Dimensions of cache memory. Same as the number of arguments of the function")
storage_info = property (lambda self: self.__zipped_info.copy(), None, None, "Information about how parameters are stored in cache.")
call_count = property (lambda self: self.__calls, None, None, "Number of times the function has been invoked (including already calculated values).")
def full_memory_stats(self):
containers = 0
null_pointers = 0
data = 0
depth = 1
def memory_inspector (container, argsleft, null, level):
nonlocal containers, null_pointers, data, depth
if depth < level:
depth = level
containers += 1
try:
iterable = container.values()
except AttributeError:
iterable = container
if argsleft:
for value in iterable:
if value is null:
null_pointers += 1
else:
memory_inspector (value, argsleft-1, null, level + 1)
else:
for value in iterable:
if value is null:
null_pointers += 1
else:
data +=1
memory_inspector(self.__container, self.__container_dimension - 1, self.__nullref, 1)
return self.__MemoryStats(containers, null_pointers, data, depth)
############################
## utility methods ##
############################
def __access_memory(self, args, kwargs):
custodian = self.__custodian
boundargs = self.__sig.bind(*args,**kwargs)
if custodian:
if not custodian(*args, **kwargs):
raise ValueError("{}() custodian function does not allow the following arguments to be stored in cache: {} {}".format(self.__function.__qualname__,args,kwargs))
boundargs.apply_defaults()
boundargs = boundargs.arguments
pos = 0
this_container = None
this_reference = self.__container
nullref = self.__nullref
for name, ctype, ofs in self.__zipped_info:
if this_reference is nullref:
this_container[pos] = this_reference = ctype()
if ofs:
pos = boundargs[name] - ofs
else:
pos = boundargs[name]
this_container = this_reference
try:
this_reference = this_container[pos]
except IndexError:
try:
this_container.extend([nullref]*(pos-len(this_container)+1))
this_reference = nullref
except Exception as e:
raise e from None
except KeyError:
try:
this_reference = this_container[pos] = nullref
except Exception as e:
raise e from None
return this_reference, this_container, pos
def __access_memory_direct(self, args, kwargs, return_bool = False):
# Does not create an entry
boundargs = self.__sig.bind(*args,**kwargs)
boundargs.apply_defaults()
boundargs = boundargs.arguments
pos = 0
this_reference = self.__container
nullref = self.__nullref
for name, ctype, ofs in self.__zipped_info:
if this_reference is nullref:
if return_bool:
return False
else:
raise KeyError("{}() has not yet calculated a value at {} {}.".format(self.__function.__qualname__,args, kwargs)) from None
if ofs:
pos = boundargs[name] - ofs
else:
pos = boundargs[name]
try:
this_reference = this_reference[pos]
except (IndexError, KeyError):
if return_bool:
return False
else:
raise KeyError("{}() has not yet calculated a value at {} {}.".format(self.__function.__qualname__,args, kwargs)) from None
if this_reference is nullref:
if return_bool:
return False
else:
raise KeyError("{}() has not yet calculated a value at {} {}.".format(self.__function.__qualname__,args, kwargs)) from None
elif return_bool:
return True
else:
return this_reference
def __subscript_to_arguments(self, key):
if type(key) is not tuple:
key=(key,)
dimensions = self.__container_dimension
try:
assert dimensions == len(key)
except AssertionError:
raise TypeError("{}() requires exactly {} arguments to access its cache memory. Given: {}.".format(self.__function.__qualname__, self.__container_dimension,len(key))) from None
lastarg = dimensions
kwargs = {}
while lastarg > 0:
arg = key[lastarg-1]
if type(arg) is slice:
k = arg.start
v = arg.stop
if k in kwargs:
raise TypeError("{}() got multiple values for keyword argument {}".format(self.__function.__qualname__, k))
else:
kwargs[k]=v
lastarg -= 1
else:
break
return key[:lastarg], kwargs
###########################
def dynamic_memory(**kwargs):
# Should have called it "memoization". Too late to back up now.
return lambda function: wraps(function)(Dynamic_Memory(function,**kwargs))