class_docstr: A class that wraps the execution of a `VM` making it easier for use as a "function".
  
  `Function` is the callable object that does computation.  It has the storage
  of inputs and outputs, performs the packing and unpacking of inputs and
  return values. It implements the square-bracket indexing so that you can
  look up the value of a symbolic node.
  
  Functions are copyable via `Function.copy` and the `copy.copy` interface.
  When a function is copied, this instance is duplicated. Contrast with
  self.maker (instance of `FunctionMaker`) that is shared between copies.
  The meaning of copying a function is that the containers and their current
  values will all be duplicated. This requires that mutable inputs be
  copied, whereas immutable inputs may be shared between copies.
  
  A Function instance is hashable, on the basis of its memory address (its
  id).
  A Function instance is only equal to itself.
  A Function instance may be serialized using the `pickle` or
  `cPickle` modules.  This will save all default inputs, the graph,
  and WRITEME to the pickle file.
  
  A `Function` instance has a `Function.trust_input` field that defaults to
  ``False``. When ``True``, the `Function` will skip all checks on the
  inputs.
  
  Attributes
  ----------
  finder
      Dictionary mapping several kinds of things to containers.
  
      We set an entry in finder for:
      - the index of the input
      - the variable instance the input is based on
      - the name of the input
  
      All entries map to the container or to DUPLICATE if an ambiguity
      is detected.
  inv_finder
      Reverse lookup of `finder`.  It maps containers to `SymbolicInput`\s.
docstr: Parameters
  ----------
  vm
      A `VM` instance that evaluates the graph when called.
  input_storage
      List of storage cells for each input.
  output_storage
      List of storage cells for each output.
  indices
      List of ``(SymbolicInput, indices, [SymbolicInput,...])``, one
      tuple for each input.  The first tuple element is the `SymbolicInput`
      object for the corresponding function input.  The second and third
      tuple elements are used only by Kits, which are deprecated.
  outputs
      TODO
  defaults
      List of 3-tuples, one 3-tuple for each input.
      Tuple element 0: ``bool``.  Is this input required at each function
      call?
      Tuple element 1: ``bool``.  Should this inputs value be reverted
      after each call?
      Tuple element 2: ``Any``.  The value associated with this input.
  unpack_single
      For outputs lists of length 1, should the 0'th element be
      returned directly?
  return_none
      Whether the function should return ``None`` or not.
  output_keys
      TODO
  maker
      The `FunctionMaker` that created this instance.
  name
      A string name.
docstr_tok: ["Parameters","--","--","--","--","--","vm","A","`","VM","`","instance","that","evaluates","the","graph","when","called",".","input_storage","List","of","storage","cells","for","each","input",".","output_storage","List","of","storage","cells","for","each","output",".","indices","List","of","``","(","SymbolicInput",",","indices",",","[","SymbolicInput",",","...","]",")","``",",","one","tuple","for","each","input",".","The","first","tuple","element","is","the","`","SymbolicInput","`","object","for","the","corresponding","function","input",".","The","second","and","third","tuple","elements","are","used","only","by","Kits",",","which","are","deprecated",".","outputs","TODO","defaults","List","of","3-tuples",",","one","3-tuple","for","each","input",".","Tuple","element","0",":","``","bool","``",".","Is","this","input","required","at","each","function","call","?","Tuple","element","1",":","``","bool","``",".","Should","this","inputs","value","be","reverted","after","each","call","?","Tuple","element","2",":","``","Any","``",".","The","value","associated","with","this","input",".","unpack_single","For","outputs","lists","of","length","1",",","should","the","0'th","element","be","returned","directly","?","return_none","Whether","the","function","should","return","``","None","``","or","not",".","output_keys","TODO","maker","The","`","FunctionMaker","`","that","created","this","instance",".","name","A","string","name","."]
code: def __init__(
      self,
      vm: "VM",
      input_storage,
      output_storage,
      indices,
      outputs,
      defaults,
      unpack_single: bool,
      return_none: bool,
      output_keys,
      maker: "FunctionMaker",
      name: Optional[str] = None,
  ):
      """
      Parameters
      ----------
      vm
          A `VM` instance that evaluates the graph when called.
      input_storage
          List of storage cells for each input.
      output_storage
          List of storage cells for each output.
      indices
          List of ``(SymbolicInput, indices, [SymbolicInput,...])``, one
          tuple for each input.  The first tuple element is the `SymbolicInput`
          object for the corresponding function input.  The second and third
          tuple elements are used only by Kits, which are deprecated.
      outputs
          TODO
      defaults
          List of 3-tuples, one 3-tuple for each input.
          Tuple element 0: ``bool``.  Is this input required at each function
          call?
          Tuple element 1: ``bool``.  Should this inputs value be reverted
          after each call?
          Tuple element 2: ``Any``.  The value associated with this input.
      unpack_single
          For outputs lists of length 1, should the 0'th element be
          returned directly?
      return_none
          Whether the function should return ``None`` or not.
      output_keys
          TODO
      maker
          The `FunctionMaker` that created this instance.
      name
          A string name.
      """
      # TODO: Rename to `vm`
      self.vm = vm
      self.input_storage = input_storage
      self.output_storage = output_storage
      self.indices = indices
      self.outputs = outputs
      self.defaults = defaults
      self.unpack_single = unpack_single
      self.return_none = return_none
      self.maker = maker
      self.profile = None  # reassigned in FunctionMaker.create
      self.trust_input = (
          False  # If True, we don't check the input parameter
      )
      self.name = name
      self.nodes_with_inner_function = []
      self.output_keys = output_keys
  
      # See if we have any mutable / borrow inputs
      # TODO: this only need to be set if there is more than one input
      self._check_for_aliased_inputs = False
      for i in maker.inputs:
          # If the input is a shared variable, the memory region is
          # under PyTensor control and so we don't need to check if it
          # is aliased as we never do that.
          if (
              isinstance(i, In)
              and not i.shared
              and (
                  getattr(i, "borrow", False)
                  or getattr(i, "mutable", False)
              )
          ):
              self._check_for_aliased_inputs = True
              break
  
      # We will be popping stuff off this `containers` object.  It is a copy.
      containers = list(self.input_storage)
      finder = {}
      inv_finder = {}
  
      def distribute(indices, cs, value):
          input.distribute(value, indices, cs)
          for c in cs:
              c.provided += 1
  
      # Store the list of names of named inputs.
      named_inputs = []
      # Count the number of un-named inputs.
      n_unnamed_inputs = 0
  
      # Initialize the storage
      # this loop works by modifying the elements (as variable c) of
      # self.input_storage inplace.
      for i, (
          (input, indices, sinputs),
          (required, refeed, value),
      ) in enumerate(zip(self.indices, defaults)):
          if indices is None:
              # containers is being used as a stack. Here we pop off
              # the next one.
              c = containers[0]
              c.strict = getattr(input, "strict", False)
              c.allow_downcast = getattr(input, "allow_downcast", None)
  
              if value is not None:
                  # Always initialize the storage.
                  if isinstance(value, Container):
                      # There is no point in obtaining the current value
                      # stored in the container, since the container is
                      # shared.
                      # For safety, we make sure 'refeed' is False, since
                      # there is no need to refeed the default value.
                      assert not refeed
                  else:
                      c.value = value
              c.required = required
              c.implicit = input.implicit
              # this is a count of how many times the input has been
              # provided (reinitialized to 0 on __call__)
              c.provided = 0
              finder[i] = c
              finder[input.variable] = c
              if input.name not in finder:
                  finder[input.name] = c
              else:
                  finder[input.name] = DUPLICATE
              if input.name is None:
                  n_unnamed_inputs += 1
              else:
                  named_inputs.append(input.name)
              inv_finder[c] = input
              containers[:1] = []
  
      self.finder = finder
      self.inv_finder = inv_finder
  
      # this class is important in overriding the square-bracket notation:
      #     fn.value[x]
      # self reference is available via the closure on the class
      class ValueAttribute:
          def __getitem__(self, item):
              try:
                  s = finder[item]
              except KeyError:
                  raise TypeError(f"Unknown input or state: {item}")
              if s is DUPLICATE:
                  raise TypeError(
                      f"Ambiguous name: {item} - please check the "
                      "names of the inputs of your function "
                      "for duplicates."
                  )
              if isinstance(s, Container):
                  return s.value
              else:
                  raise NotImplementedError
  
          def __setitem__(self, item, value):
              try:
                  s = finder[item]
              except KeyError:
                  # Print informative error message.
                  msg = get_info_on_inputs(
                      named_inputs, n_unnamed_inputs
                  )
                  raise TypeError(
                      f"Unknown input or state: {item}. {msg}"
                  )
              if s is DUPLICATE:
                  raise TypeError(
                      f"Ambiguous name: {item} - please check the "
                      "names of the inputs of your function "
                      "for duplicates."
                  )
              if isinstance(s, Container):
                  s.value = value
                  s.provided += 1
              else:
                  s(value)
  
          def __contains__(self, item):
              return finder.__contains__(item)
  
      # this class is important in overriding the square-bracket notation:
      #     fn.container[x]
      # self reference is available via the closure on the class
      class ContainerAttribute:
          def __getitem__(self, item):
              return finder[item]
  
          def __contains__(self, item):
              return finder.__contains__(item)
  
          # You cannot set the container
  
      self._value = ValueAttribute()
      self._container = ContainerAttribute()
  
      # TODO: Get rid of all this `expanded_inputs` nonsense
      assert len(self.maker.expanded_inputs) == len(self.input_storage)
  
      # This is used only when `vm.need_update_inputs` is `False`, because
      # we're using one of the VM objects and it is putting updates back into
      # the input containers all by itself.
      self.n_returned_outputs = len(self.output_storage) - sum(
          inp.update is not None for inp in self.maker.expanded_inputs
      )
  
      for node in self.maker.fgraph.apply_nodes:
          if isinstance(node.op, HasInnerGraph):
              self.nodes_with_inner_function.append(node.op)
  
imports: import copy
  import copyreg
  import logging
  import time
  import warnings
  from itertools import chain
  from typing import TYPE_CHECKING, Optional
  import numpy
  import pytensor
  import pytensor.compile.profiling
  from pytensor.compile.io import In, SymbolicInput, SymbolicOutput
  from pytensor.compile.ops import deep_copy_op, view_op
  from pytensor.configdefaults import config
  from pytensor.graph.basic import Constant, Variable, ancestors, clone_get_equiv, graph_inputs
  from pytensor.graph.destroyhandler import DestroyHandler
  from pytensor.graph.features import AlreadyThere, Feature, PreserveVariableAttributes
  from pytensor.graph.fg import FunctionGraph
  from pytensor.graph.op import HasInnerGraph
  from pytensor.graph.utils import InconsistencyError, get_variable_trace_string
  from pytensor.link.basic import Container
  from pytensor.link.utils import raise_with_op
label_desc: Use image node_id 1 to create a new Function object  with example: obj = Function(vm, input_storage, output_storage, indices, outputs, defaults, unpack_single, return_none, output_keys, maker, name)