docs: def str(self, format=None, unit=None, scientific=False, si=False, labels=True, style="plain",
              styles=None, force_asymmetric=False, **kwargs):
          r"""
          Returns a readable string representiation of the number. *format* is used to format
          non-NumPy nominal and uncertainty values. It can be a string such as ``"%d"``, a function
          that is called with the value to format, or a rounding method as accepted by
          :py:meth:`round_value`. When *None* (the default), :py:attr:`default_format` is used. All
          keyword arguments except wildcard *kwargs* are only used to format non-NumPy values. In case
          of NumPy objects, *kwargs* are passed to `numpy.array2string
          <https://docs.scipy.org/doc/numpy/reference/generated/numpy.array2string.html>`_.
  
          When *unit* is set, it is appended to the end of the string. When *scientific* is *True*,
          all values are represented by their scientific notation. When *scientific* is *False* and
          *si* is *True*, the appropriate SI prefix is used. *labels* controls whether uncertainty
          labels are shown in the string. When *True*, uncertainty names are used, but it can also
          be a list of labels whose order should match the uncertainty dict traversal order. *style*
          can be ``"plain"``, ``"latex"``, or ``"root"``. *styles* can be a dict with fields
          ``"space"``, ``"label"``, ``"unit"``, ``"sym"``, ``"asym"``, ``"sci"`` to customize every
          aspect of the format style on top of :py:attr:`style_dict`. Unless *force_asymmetric* is
          *True*, an uncertainty is quoted symmetric if it yields identical values in both directions.
  
          Examples:
  
          .. code-block:: python
  
              n = Number(17.321, {"a": 1.158, "b": 0.453})
              n.str()               # -> '17.321 +- 1.158 (a) +- 0.453 (b)'
              n.str("%.1f")         # -> '17.3 +- 1.2 (a) +- 0.5 (b)'
              n.str("publication")  # -> '17.32 +- 1.16 (a) +- 0.45 (b)'
              n.str("pdg")          # -> '17.3 +- 1.2 (a) +- 0.5 (b)'
  
              n = Number(8848, 10)
              n.str(unit="m")                           # -> "8848.0 +- 10.0 m"
              n.str(unit="m", force_asymmetric=True)    # -> "8848.0 +10.0-10.0 m"
              n.str(unit="m", scientific=True)          # -> "8.848 +- 0.01 x 1E3 m"
              n.str("%.2f", unit="m", scientific=True)  # -> "8.85 +- 0.01 x 1E3 m"
              n.str(unit="m", si=True)                  # -> "8.848 +- 0.01 km"
              n.str("%.2f", unit="m", si=True)          # -> "8.85 +- 0.01 km"
              n.str(unit="m", style="latex")            # -> "8848.0 \pm 10.0\,m"
              n.str(unit="m", style="latex", si=True)   # -> "8.848 \pm 0.01\,km"
              n.str(unit="m", style="root")             # -> "8848.0 #pm 10.0 m"
              n.str(unit="m", style="root", si=True)    # -> "8.848 #pm 0.01 km"
          """
          if format is None:
              format = self.default_format or self.__class__.default_format
  
          if not self.is_numpy:
              # check style
              style = style.lower()
              if style not in style_dict.keys():
                  raise ValueError("unknown style '{}'".format(style))
              d = style_dict[style]
  
              # extend by custom styles
              if styles:
                  d.update(styles)
  
              # scientific or SI notation?
              prefix = ""
              transform = lambda x: x
              if scientific or si:
                  if scientific:
                      mag = 0 if self.nominal == 0 else int(math.floor(math.log10(abs(self.nominal))))
                  else:
                      prefix, mag = infer_si_prefix(self.nominal)
                  transform = lambda x: x * 10.**(-mag)
  
              # gather and transform values
              nominal = transform(self.nominal)
              names, ups, downs = [], [], []
              for name, (up, down) in self.uncertainties.items():
                  names.append(name)
                  ups.append(transform(up))
                  downs.append(transform(down))
  
              # special formats implemented by round_value
              if format in ("pub", "publication", "pdg", "one", "onedigit"):
                  nominal, (ups, downs), _mag = round_value(self.nominal, ups, downs, method=format)
                  fmt = lambda x: match_precision(float(x) * 10.**_mag, 10.**_mag)
  
              # string formatting
              elif not callable(format):
                  fmt = lambda x: format % x
  
              # helper to build the ending consisting of scientific notation or SI prefix, and unit
              def ending():
                  e = ""
                  if scientific and mag:
                      e += d["space"] + d["sci"].format(mag=mag)
                  _unit = (prefix or "") + (unit or "")
                  if _unit:
                      e += d["unit"].format(unit=_unit)
                  return e
  
              # start building the text
              text = fmt(nominal, **kwargs)
  
              # no uncertainties
              if len(names) == 0:
                  text += ending()
                  if style == "plain" and labels:
                      text += d["space"] + d["label"].format(label="no uncertainties")
  
              # one ore more uncertainties
              else:
                  # special case: only the default uncertainty
                  if len(names) == 1 and names[0] == self.DEFAULT:
                      labels = False
  
                  for i, (name, up, down) in enumerate(zip(names, ups, downs)):
                      up = str(fmt(up))
                      down = str(fmt(down))
  
                      if up == down and not force_asymmetric:
                          text += d["space"] + d["sym"].format(unc=up)
                      else:
                          text += d["space"] + d["asym"].format(up=up, down=down)
  
                      if labels:
                          label = labels[i] if isinstance(labels, (list, tuple)) else name
                          text += d["space"] + d["label"].format(label=label)
  
                  text += ending()
  
              return text
  
          else:
              # we are dealing with a numpy array here
              # start with nominal text
              text = np.array2string(self.nominal, **kwargs)
  
              # uncertainty text
              uncs = self.uncertainties
              if len(uncs) == 0:
                  text += " (no uncertainties)"
              elif len(uncs) == 1 and list(uncs.keys())[0] == self.DEFAULT:
                  up, down = self.get_uncertainty()
                  text += "\n+ {}".format(np.array2string(up, **kwargs))
                  text += "\n- {}".format(np.array2string(down, **kwargs))
              else:
                  for name, (up, down) in uncs.items():
                      text += "\n+ {} {}".format(name, np.array2string(up, **kwargs))
                      text += "\n- {} {}".format(name, np.array2string(down, **kwargs))
  
              return text