Repository: networkx/networkx

Issue:
Critical NetworkX 2.8.X bug with mutable cached_properties
### Current Behavior

The `nodes()` method of a Graph is decorated with `@cached_property`.<br>
This leads to the assumption that a Graph's `nodes()` method should return a static value.<br>
This assumption is incorrect.

Notably, the `@cached_property` decorator completely breaks `Graph.subgraph()`.<br>
Trace:

Graph.subgraph -> graphviews.subgraph_view -> [this line](https://github.com/networkx/networkx/blob/ead0e65bda59862e329f2e6f1da47919c6b07ca9/networkx/classes/graphviews.py#L149) prevents [this line](https://github.com/networkx/networkx/blob/ead0e65bda59862e329f2e6f1da47919c6b07ca9/networkx/classes/graphviews.py#L152) from functioning correctly

Subgraphs are shown as containing the wrong nodes as a result, until `del G.nodes` is manually run.<br>
This breaks things.

### Expected Behavior

`@cached_property` decorators should not break functionality.

### Steps to Reproduce

- Initialize a Graph, `G`
- Populate `G` with node objects that are complex enough that the following inequality assertion will pass: `newG = nx.freeze(G.__class__()); newG._graph = G; newG.graph = G.graph; assert(newG.nodes()!=G.nodes)`.
- Pick a subset of `G.nodes`, call this `subG_nodes`
- `subgraph = G.subgraph(subG_nodes)`
- In NetworkX 2.7, the nodes of `subgraph` will be a subset of the nodes of `G`. In NetworkX 2.8, the nodes of `subgraph` and the nodes of `G` will be a fully disjoint set.

### Environment
<!--- Please provide details about your local environment -->
Python version: 3.9.10
NetworkX version: 2.8.5 vs 2.7.0



Discussion:
Thank you!  This is indeed a bug. We will need to make a similar fix to the ones we made for G.adj/G.succ/G.pred
That is, add a data descriptor for G._node which resets the cache when G._node is set.
The workaround until a fix is ready is:
```
subgraph = G.subgraph(subG_nodes)
del subgraph.nodes
```

Resolving patch:
diff --git a/networkx/classes/graph.py b/networkx/classes/graph.py
--- a/networkx/classes/graph.py
+++ b/networkx/classes/graph.py
@@ -41,6 +41,28 @@ def __set__(self, obj, value):
             del od["adj"]
 
 
+class _CachedPropertyResetterNode:
+    """Data Descriptor class for _node that resets ``nodes`` cached_property when needed
+
+    This assumes that the ``cached_property`` ``G.node`` should be reset whenever
+    ``G._node`` is set to a new value.
+
+    This object sits on a class and ensures that any instance of that
+    class clears its cached property "nodes" whenever the underlying
+    instance attribute "_node" is set to a new object. It only affects
+    the set process of the obj._adj attribute. All get/del operations
+    act as they normally would.
+
+    For info on Data Descriptors see: https://docs.python.org/3/howto/descriptor.html
+    """
+
+    def __set__(self, obj, value):
+        od = obj.__dict__
+        od["_node"] = value
+        if "nodes" in od:
+            del od["nodes"]
+
+
 class Graph:
     """
     Base class for undirected graphs.
@@ -282,6 +304,7 @@ class Graph:
     """
 
     _adj = _CachedPropertyResetterAdj()
+    _node = _CachedPropertyResetterNode()
 
     node_dict_factory = dict
     node_attr_dict_factory = dict


Test patch:
diff --git a/networkx/classes/tests/test_graph.py b/networkx/classes/tests/test_graph.py
--- a/networkx/classes/tests/test_graph.py
+++ b/networkx/classes/tests/test_graph.py
@@ -178,6 +178,11 @@ def test_cache_reset(self):
         G._adj = {}
         assert id(G.adj) != id(old_adj)
 
+        old_nodes = G.nodes
+        assert id(G.nodes) == id(old_nodes)
+        G._node = {}
+        assert id(G.nodes) != id(old_nodes)
+
     def test_attributes_cached(self):
         G = self.K3.copy()
         assert id(G.nodes) == id(G.nodes)