docs: def doprint(self, expr, *args, **kwargs):
          """Returns printer's representation for expr (as a string)
  
          The representation is obtained by the following methods:
  
          1. from the :attr:`cache`
          2. If `expr` is a Sympy object, delegate to the
             :meth:`~sympy.printing.printer.Printer.doprint` method of
             :attr:`_sympy_printer`
          3. Let the `expr` print itself if has the :attr:`printmethod`
          4. Take the best fitting ``_print_*`` method of the printer
          5. As fallback, delegate to :meth:`emptyPrinter`
  
          Any extra `args` or `kwargs` are passed to the internal `_print`
          method.
          """
          allow_caching = self._allow_caching
          is_cached = False
          if len(args) > 0 or len(kwargs) > 0:
              # we don't want to cache "custom" rendering, such as the adjoint of
              # the actual expression (kwargs['adjoint'] is True). Otherwise, we
              # might return a cached values for args/kwargs that are different
              # from the the expression was originally cached.
              allow_caching = False
  
          if allow_caching:
              is_cached, res = self._get_from_cache(expr)
          if not is_cached:
              if isinstance(expr, Scalar._val_types):
                  res = self._print_SCALAR_TYPES(expr, *args, **kwargs)
              elif isinstance(expr, str):
                  return self._render_str(expr)
              else:
                  # the _print method, inherited from SympyPrinter implements the
                  # internal dispatcher for (3-5)
                  res = self._str(self._print(expr, *args, **kwargs))
              if allow_caching:
                  self._write_to_cache(expr, res)
          return res