class_docstr: A `FunctionGraph` represents a subgraph bound by a set of input variables and
  a set of output variables, ie a subgraph that specifies an PyTensor function.
  The inputs list should contain all the inputs on which the outputs depend.
  `Variable`\s of type `Constant` are not counted as inputs.
  
  The `FunctionGraph` supports the replace operation which allows to replace
  a variable in the subgraph by another, e.g. replace ``(x + x).out`` by
  ``(2 * x).out``. This is the basis for optimization in PyTensor.
  
  This class is also responsible for verifying that a graph is valid
  (ie, all the dtypes and broadcast patterns are compatible with the
  way the `Variable`\s are used) and for tracking the `Variable`\s with
  a :attr:`FunctionGraph.clients` ``dict`` that specifies which `Apply` nodes
  use the `Variable`.  The :attr:`FunctionGraph.clients` field, combined with
  the :attr:`Variable.owner` and each :attr:`Apply.inputs`, allows the graph
  to be traversed in both directions.
  
  It can also be extended with new features using
  :meth:`FunctionGraph.attach_feature`.  See `Feature` for event types and
  documentation.  Extra features allow the `FunctionGraph` to verify new
  properties of a graph as it is optimized.
  
  The constructor creates a `FunctionGraph` which operates on the subgraph
  bound by the inputs and outputs sets.
  
  This class keeps lists for the inputs and outputs and modifies them
  in-place.
docstr: Add a ``graph.features.Feature`` to this function graph and trigger its ``on_attach`` callback.
docstr_tok: ["Add","a","``","graph.features.Feature","``","to","this","function","graph","and","trigger","its","``","on_attach","``","callback","."]
code: def attach_feature(self, feature: Feature) -> None:
      """Add a ``graph.features.Feature`` to this function graph and trigger its ``on_attach`` callback."""
      # Filter out literally identical `Feature`s
      if feature in self._features:
          return  # the feature is already present
  
      # Filter out functionally identical `Feature`s.
      # `Feature`s may use their `on_attach` method to raise
      # `AlreadyThere` if they detect that some
      # installed `Feature` does the same thing already
      attach = getattr(feature, "on_attach", None)
      if attach is not None:
          try:
              attach(self)
          except AlreadyThere:
              return
      self.execute_callbacks_times.setdefault(feature, 0.0)
      # It would be nice if we could require a specific class instead of
      # a "workalike" so we could do actual error checking
      # if not isinstance(feature, Feature):
      #    raise TypeError("Expected Feature instance, got "+\
      #            str(type(feature)))
  
      # Add the feature
      self._features.append(feature)
  
imports: import time
  from collections import OrderedDict
  from collections.abc import Iterable, Sequence
  from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast
  import pytensor
  from pytensor.configdefaults import config
  from pytensor.graph.basic import Apply, AtomicVariable, Variable, applys_between, clone_get_equiv, graph_inputs, io_toposort, vars_between
  from pytensor.graph.basic import as_string
  from pytensor.graph.features import AlreadyThere, Feature, ReplaceValidate
  from pytensor.graph.utils import MetaObject, MissingInputError, TestValueError
  from pytensor.misc.ordered_set import OrderedSet
label_desc: Use image node_id 17 for calling the FunctionGraph obj's underlying member method code with example usage: obj.attach_feature(feature) without return types