Skip to content

Commit 041033f

Browse files
authored
Merge pull request #62 from petercorke/fix/vertex-init-setattr
perf: a vertex fills its attributes directly in __init__ and add_vertex
2 parents 1cc79f3 + ed5af4f commit 041033f

2 files changed

Lines changed: 163 additions & 18 deletions

File tree

‎src/pgraph/PGraph.py‎

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,12 +1232,14 @@ def add_vertex(
12321232
name = f"#{n}"
12331233
elif name in self._vertexdict:
12341234
raise ValueError(f"the graph already has a vertex named {name!r}")
1235-
vertex.name = name
1235+
# the vertex is in no graph yet, so the name table and the spatial index
1236+
# have nothing to be told; skip the property and ``__setattr__``
1237+
vertex.__dict__["_name"] = name
12361238
self._vertexlist.append(vertex)
1237-
self._vertexdict[vertex.name] = vertex
1239+
self._vertexdict[name] = vertex
12381240
if self._verbose:
1239-
print(f"New vertex {vertex.name}: {vertex.coord}")
1240-
vertex._graph = self
1241+
print(f"New vertex {name}: {vertex.coord}")
1242+
vertex.__dict__["_graph"] = self
12411243
self._connectivitychange = True
12421244
self._spatialchange = True
12431245
return vertex
@@ -4466,6 +4468,14 @@ class Vertex:
44664468
:seealso: :class:`Graph` :class:`Edge`
44674469
"""
44684470

4471+
coord: NDArray | None
4472+
_name: str | None
4473+
_label: int | None
4474+
_connectivitychange: bool
4475+
_edgelist: list[Edge]
4476+
_inlist: list[Edge]
4477+
_graph: Graph | None
4478+
44694479
def __init__(self, coord: ArrayLike | None = None, name: str | None = None):
44704480
"""
44714481
Create a vertex object
@@ -4486,20 +4496,21 @@ def __init__(self, coord: ArrayLike | None = None, name: str | None = None):
44864496
44874497
:seealso: :meth:`Graph.add_vertex`
44884498
"""
4489-
self._edgelist: list[Edge] = []
4490-
if coord is None:
4491-
self.coord = None
4492-
else:
4493-
self.coord = np.r_[coord]
4494-
self._name = name
4495-
self._label: int | None = None
4496-
self._connectivitychange = True
4497-
self._edgelist = []
4498-
#: edges ending at this vertex. Only a directed vertex fills it: an
4499-
#: undirected edge is already in ``_edgelist`` at both of its ends.
4500-
self._inlist: list[Edge] = []
4501-
self._graph: Graph | None = None # reference to owning graph
4502-
# print('Vertex init', type(self))
4499+
# A vertex that is being made is in no graph, so there is no spatial
4500+
# index (Graph.build_kdtree) for the assignment of its coordinate to
4501+
# invalidate; writing the attributes directly avoids the cost of
4502+
# ``__setattr__`` on each of them.
4503+
self.__dict__.update(
4504+
coord=None if coord is None else np.r_[coord],
4505+
_name=name,
4506+
_label=None,
4507+
_connectivitychange=True,
4508+
_edgelist=[], # edges leaving the vertex, or incident on it if undirected
4509+
# edges ending at this vertex. Only a directed vertex fills it: an
4510+
# undirected edge is already in ``_edgelist`` at both of its ends.
4511+
_inlist=[],
4512+
_graph=None, # reference to owning graph
4513+
)
45034514

45044515
def __setattr__(self, name: str, value: Any) -> None:
45054516
# a vertex that moves makes its graph's spatial index (build_kdtree)

‎tests/test_vertex_init.py‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"""
2+
A vertex fills its attributes directly in ``__init__``, and ``Graph.add_vertex``
3+
sets its name and graph directly, because the ``__setattr__`` of a vertex only
4+
exists to tell the graph that a coordinate moved, and a vertex that is not in
5+
a graph yet has no graph to tell. These tests check that nothing else changed:
6+
the attributes, the name table, and that moving a vertex that is in a graph is
7+
still seen.
8+
"""
9+
10+
import contextlib
11+
import io
12+
import unittest
13+
14+
import numpy as np
15+
16+
from pgraph import DGraph, UGraph, Vertex
17+
18+
19+
class TestConstructor(unittest.TestCase):
20+
def test_attributes_of_a_new_vertex(self):
21+
v = Vertex(coord=[1, 2], name="a")
22+
np.testing.assert_array_equal(v.coord, [1, 2])
23+
self.assertEqual(v.name, "a")
24+
self.assertIsNone(v.label)
25+
self.assertEqual(v._edgelist, [])
26+
self.assertEqual(v._inlist, [])
27+
self.assertIsNone(v._graph)
28+
self.assertTrue(v._connectivitychange)
29+
30+
def test_no_coordinate(self):
31+
v = Vertex()
32+
self.assertIsNone(v.coord)
33+
self.assertIsNone(v.name)
34+
35+
def test_the_coordinate_is_a_float_array_copy(self):
36+
c = np.array([1.0, 2.0])
37+
v = Vertex(coord=c)
38+
c[0] = 99.0
39+
self.assertEqual(v.coord[0], 1.0) # not the caller's array
40+
self.assertIsInstance(v.coord, np.ndarray)
41+
42+
def test_the_lists_are_not_shared_between_vertices(self):
43+
a, b = Vertex(), Vertex()
44+
self.assertIsNot(a._edgelist, b._edgelist)
45+
self.assertIsNot(a._inlist, b._inlist)
46+
self.assertIsNot(a._edgelist, a._inlist)
47+
48+
def test_subclass_with_its_own_init(self):
49+
class Landmark(Vertex):
50+
def __init__(self, coord=None, name=None, color=None):
51+
super().__init__(coord, name)
52+
self.color = color # an attribute of its own, set after
53+
54+
v = Landmark([0.0, 0.0], name="l", color="red")
55+
self.assertEqual((v.name, v.color), ("l", "red"))
56+
g = UGraph(vertex_class=Landmark)
57+
g.add_vertex(v)
58+
self.assertEqual(v.color, "red")
59+
self.assertIs(g["l"], v)
60+
61+
62+
class TestInAGraph(unittest.TestCase):
63+
def test_name_table_and_back_pointer(self):
64+
for cls in (UGraph, DGraph):
65+
with self.subTest(cls=cls.__name__):
66+
g = cls()
67+
v = g.add_vertex([0.0, 0.0], name="v")
68+
w = g.add_vertex([1.0, 0.0]) # default name
69+
self.assertIs(v._graph, g)
70+
self.assertIs(g["v"], v)
71+
self.assertEqual(w.name, "#1")
72+
self.assertIs(g["#1"], w)
73+
self.assertEqual(g.validate(), [])
74+
75+
def test_name_from_the_vertex_is_kept(self):
76+
g = UGraph()
77+
v = Vertex(name="given")
78+
g.add_vertex(v)
79+
self.assertEqual(v.name, "given")
80+
self.assertIs(g["given"], v)
81+
82+
def test_renaming_still_updates_the_table(self):
83+
g = UGraph()
84+
v = g.add_vertex(name="old")
85+
v.name = "new"
86+
self.assertIs(g["new"], v)
87+
self.assertNotIn("old", g)
88+
89+
def test_duplicate_name_is_refused_and_the_vertex_stays_free(self):
90+
g = UGraph()
91+
g.add_vertex(name="a")
92+
v = Vertex(name="b")
93+
with self.assertRaises(ValueError):
94+
g.add_vertex(v, name="a")
95+
self.assertIsNone(v._graph)
96+
self.assertEqual(v.name, "b")
97+
98+
def test_a_vertex_in_a_graph_cannot_be_added_to_another(self):
99+
g, h = UGraph(), UGraph()
100+
v = g.add_vertex()
101+
with self.assertRaises(ValueError):
102+
h.add_vertex(v)
103+
104+
def test_a_verbose_graph_says_what_it_adds(self):
105+
g = UGraph(dim=2, verbose=True)
106+
buf = io.StringIO()
107+
with contextlib.redirect_stdout(buf):
108+
g.add_vertex([1.0, 2.0], name="a")
109+
g.add_vertex([3.0, 4.0])
110+
lines = buf.getvalue().splitlines()
111+
self.assertEqual(len(lines), 2)
112+
self.assertTrue(lines[0].startswith("New vertex a: "))
113+
self.assertTrue(lines[1].startswith("New vertex #1: "))
114+
115+
def test_assigning_a_coordinate_is_still_seen(self):
116+
g = UGraph(dim=2)
117+
v = g.add_vertex([0.0, 0.0])
118+
g._spatialchange = False
119+
v.coord = np.r_[1.0, 1.0]
120+
self.assertTrue(g._spatialchange)
121+
g._spatialchange = False
122+
v.coord += np.r_[1.0, 0.0]
123+
self.assertTrue(g._spatialchange)
124+
125+
def test_assigning_other_attributes_is_not_a_change(self):
126+
g = UGraph(dim=2)
127+
v = g.add_vertex([0.0, 0.0])
128+
g._spatialchange = False
129+
v.data = "anything"
130+
self.assertFalse(g._spatialchange)
131+
132+
133+
if __name__ == "__main__":
134+
unittest.main()

0 commit comments

Comments
 (0)