docs: def cached_func(fname=None, cache_dir='default', appname='utool', key_argx=None,
                  key_kwds=None, use_cache=None, verbose=None):
      r"""
      Wraps a function with a Cacher object
  
      uses a hash of arguments as input
  
      Args:
          fname (str):  file name (defaults to function name)
          cache_dir (unicode): (default = u'default')
          appname (unicode): (default = u'utool')
          key_argx (None): (default = None)
          key_kwds (None): (default = None)
          use_cache (bool):  turns on disk based caching(default = None)
  
      CommandLine:
          python -m utool.util_cache --exec-cached_func
  
      Example:
          >>> # ENABLE_DOCTEST
          >>> import utool as ut
          >>> def costly_func(a, b, c='d', *args, **kwargs):
          ...     return ([a] * b, c, args, kwargs)
          >>> ans0 = costly_func(41, 3)
          >>> ans1 = costly_func(42, 3)
          >>> closure_ = ut.cached_func('costly_func', appname='utool_test',
          >>>                           key_argx=[0, 1])
          >>> efficient_func = closure_(costly_func)
          >>> ans2 = efficient_func(42, 3)
          >>> ans3 = efficient_func(42, 3)
          >>> ans4 = efficient_func(41, 3)
          >>> ans5 = efficient_func(41, 3)
          >>> assert ans1 == ans2
          >>> assert ans2 == ans3
          >>> assert ans5 == ans4
          >>> assert ans5 == ans0
          >>> assert ans1 != ans0
      """
      if verbose is None:
          verbose = VERBOSE_CACHE
      def cached_closure(func):
          from utool import util_decor
          import utool as ut
          fname_ = util_inspect.get_funcname(func) if fname is None else fname
          kwdefaults = util_inspect.get_kwdefaults(func)
          argnames   = util_inspect.get_argnames(func)
          if ut.is_method(func):
              # ignore self for methods
              argnames = argnames[1:]
          cacher = Cacher(fname_, cache_dir=cache_dir, appname=appname,
                          verbose=verbose)
          if use_cache is None:
              use_cache_ = not util_arg.get_argflag('--nocache-' + fname_)
          else:
              use_cache_ = use_cache
          #_dbgdict = dict(fname_=fname_, key_kwds=key_kwds, appname=appname,
          #                key_argx=key_argx, use_cache_=use_cache_)
          #@functools.wraps(func)
          def cached_wraper(*args, **kwargs):
              """
              Cached Wrapper Function
  
              Additional Kwargs:
                  use_cache (bool) : enables cache
              """
              try:
                  if verbose > 2:
                      print('[util_cache] computing cached function fname_=%s' %
                            ( fname_,))
                  # Implicitly adds use_cache to kwargs
                  cfgstr = get_cfgstr_from_args(func, args, kwargs, key_argx,
                                                key_kwds, kwdefaults, argnames)
                  if util_cplat.WIN32:
                      # remove potentially invalid chars
                      cfgstr = '_' + util_hash.hashstr27(cfgstr)
                  assert cfgstr is not None, 'cfgstr=%r cannot be None' % (cfgstr,)
                  use_cache__ = kwargs.pop('use_cache', use_cache_)
                  if use_cache__:
                      # Make cfgstr from specified input
                      data = cacher.tryload(cfgstr)
                      if data is not None:
                          return data
                  # Cached missed compute function
                  data = func(*args, **kwargs)
                  # Cache save
                  #if use_cache__:
                  # TODO: save_cache
                  cacher.save(data, cfgstr)
                  return data
              #except ValueError as ex:
              # handle protocal error
              except Exception as ex:
                  from utool import util_dbg
                  _dbgdict2 = dict(key_argx=key_argx, lenargs=len(args),
                                   lenkw=len(kwargs),)
                  msg = '\n'.join([
                      '+--- UTOOL --- ERROR IN CACHED FUNCTION',
                      #'dbgdict = ' + utool.repr4(_dbgdict),
                      'dbgdict2 = ' + util_str.repr4(_dbgdict2),
                  ])
                  util_dbg.printex(ex, msg)
                  raise
          # Give function a handle to the cacher object
          cached_wraper = util_decor.preserve_sig(cached_wraper, func)
          cached_wraper.cacher = cacher
          return cached_wraper
      return cached_closure