code: def estimate_size(cls, ctx: dict, op: OperandType):
      from .fetch import FetchShuffle
  
      # when sizes of all outputs are deterministic, return directly
      outputs = op.outputs
      if all(
          not c.is_sparse()
          and hasattr(c, "nbytes")
          and not np.isnan(c.nbytes)
          for c in outputs
      ):
          for out in outputs:
              ctx[out.key] = (out.nbytes, out.nbytes)
          return
  
      pure_dep_keys = set(
          inp.key
          for inp, is_dep in zip(op.inputs or (), op.pure_depends or ())
          if is_dep
      )
      exec_sizes = [0]
      for inp in op.inputs or ():
          if inp.key in pure_dep_keys:
              continue
          try:
              if isinstance(inp.op, FetchShuffle):
                  keys_and_shapes = inp.extra_params.get(
                      "_shapes", dict()
                  ).items()
              else:
                  keys_and_shapes = [
                      (inp.key, getattr(inp, "shape", None))
                  ]
  
              # execution size of a specific data chunk may be
              # larger than stored type due to objects
              for key, shape in keys_and_shapes:
                  exec_sizes.append(ctx[key][0])
          except KeyError:
              if not op.sparse:
                  inp_size = calc_data_size(inp)
                  if not np.isnan(inp_size):
                      exec_sizes.append(inp_size)
      if any(c.is_sparse() for c in op.inputs):
          exec_size = sum(exec_sizes)
      else:
          exec_size = max(exec_sizes)
  
      total_out_size = 0
      chunk_sizes = dict()
      for out in outputs:
          try:
              if not out.is_sparse():
                  chunk_size = calc_data_size(out)
              else:
                  chunk_size = exec_size
              if np.isnan(chunk_size):
                  raise TypeError
              chunk_sizes[out.key] = chunk_size
              total_out_size += chunk_size
          except (AttributeError, TypeError, ValueError):
              pass
  
      exec_size = max(exec_size, total_out_size)
      memory_scale = op.memory_scale or 1.0
      for out in outputs:
          if out.key in ctx:
              continue
          if out.key in chunk_sizes:
              result_size = chunk_sizes[out.key]
          else:
              result_size = max(
                  exec_size // len(outputs),
                  total_out_size // max(len(chunk_sizes), 1),
              )
          try:
              if (
                  getattr(out, "dtype", None) is not None
                  and out.is_sparse()
              ):
                  max_sparse_size = (
                      out.nbytes
                      + np.dtype(np.int64).itemsize
                      * np.prod(out.shape)
                      * out.ndim
                  )
              else:
                  max_sparse_size = np.nan
          except TypeError:  # pragma: no cover
              max_sparse_size = np.nan
          if not np.isnan(max_sparse_size):
              result_size = min(result_size, max_sparse_size)
          ctx[out.key] = (
              result_size,
              int(exec_size * memory_scale // len(outputs)),
          )
  
imports: import sys
  from typing import Any, Callable, Dict, List, Tuple, Type, Union
  import numpy
  from ...metrics import Metrics
  from ...typing import TileableType, ChunkType, OperandType
  from ...utils import calc_data_size
  from ..context import Context
  from ..mode import is_eager_mode
  from ..entity import OutputType, ExecutableTuple, get_chunk_types, get_tileable_types, get_output_types, get_fetch_class
label_desc: Use image node_id 19 for calling the TileableOperandMixin obj's underlying member method code with example usage: obj.estimate_size(cls, ctx, op) without return types