|
2 | 2 |
|
3 | 3 | from typing import TYPE_CHECKING |
4 | 4 | import inspect |
| 5 | +from functools import lru_cache |
5 | 6 |
|
6 | 7 | from ._base import _BackendAccessorBase |
7 | 8 |
|
8 | 9 | if TYPE_CHECKING: |
9 | 10 | from ..graph import AnnNet |
10 | 11 |
|
11 | 12 |
|
| 13 | +@lru_cache(maxsize=4096) |
| 14 | +def _cached_signature(fn): |
| 15 | + """inspect.signature is expensive; NX callables are stable, so memoise it.""" |
| 16 | + return inspect.signature(fn) |
| 17 | + |
| 18 | + |
| 19 | +# name -> resolved networkx callable (global; the nx module set is static). |
| 20 | +_NX_CALLABLE_CACHE: dict = {} |
| 21 | + |
| 22 | + |
12 | 23 | class _NXBackendAccessor(_BackendAccessorBase): |
13 | 24 | """NetworkX backend accessor attached to an AnnNet instance.""" |
14 | 25 |
|
@@ -108,7 +119,7 @@ def wrapper(*args, **kwargs): |
108 | 119 | args, kwargs = self._replace_owner_graph(args, kwargs, nxG) |
109 | 120 |
|
110 | 121 | try: |
111 | | - sig = inspect.signature(nx_callable) |
| 122 | + sig = _cached_signature(nx_callable) |
112 | 123 | bound = sig.bind_partial(*args, **kwargs) |
113 | 124 | except Exception: # noqa: BLE001 |
114 | 125 | bound = None |
@@ -151,9 +162,13 @@ def __dir__(self): |
151 | 162 | return sorted(set(super().__dir__()) | self._callable_names(*self._nx_candidates())) |
152 | 163 |
|
153 | 164 | def _resolve_nx_callable(self, name: str): |
| 165 | + cached = _NX_CALLABLE_CACHE.get(name) |
| 166 | + if cached is not None: |
| 167 | + return cached |
154 | 168 | for mod in self._nx_candidates(): |
155 | 169 | attr = getattr(mod, name, None) |
156 | 170 | if callable(attr): |
| 171 | + _NX_CALLABLE_CACHE[name] = attr |
157 | 172 | return attr |
158 | 173 | raise AttributeError(f"networkx has no callable '{name}'") |
159 | 174 |
|
@@ -185,7 +200,7 @@ def _nx_candidates(self): |
185 | 200 | def _needed_edge_attrs(self, target, kwargs) -> set: |
186 | 201 | needed = set() |
187 | 202 | try: |
188 | | - params = inspect.signature(target).parameters |
| 203 | + params = _cached_signature(target).parameters |
189 | 204 | except Exception: # noqa: BLE001 |
190 | 205 | params = {} |
191 | 206 | if 'weight' in params: |
@@ -290,15 +305,17 @@ def _coerce_vertices_in_bound(self, bound, nxG, label_field: str | None): |
290 | 305 | ) |
291 | 306 |
|
292 | 307 | def _map_output_vertices(self, obj): |
293 | | - _id_to_row, row_to_id = self._vertex_row_maps() |
294 | | - |
295 | 308 | def map_id(value): |
296 | 309 | # NetworkX returns vertex IDs as the backend graph's node values. |
297 | 310 | # Our backend nodes are vertex-ID strings, so strings pass through |
298 | 311 | # unchanged. Integers that NX produced from a relabel-to-int pass |
299 | | - # are mapped back to their vertex IDs. |
300 | | - if isinstance(value, int) and not isinstance(value, bool) and value in row_to_id: |
301 | | - return row_to_id[value] |
| 312 | + # are mapped back to their vertex IDs. Resolve each int in O(1) via |
| 313 | + # the graph's row->entity index instead of materialising the whole |
| 314 | + # row->id map on every call. |
| 315 | + if isinstance(value, int) and not isinstance(value, bool): |
| 316 | + vid = self._vertex_row_to_id(value) |
| 317 | + if vid is not None: |
| 318 | + return vid |
302 | 319 | return value |
303 | 320 |
|
304 | 321 | return self._map_nested_output(obj, map_id) |
|
0 commit comments