Skip to content

Commit 26503ff

Browse files
Copilotdjeada
andcommitted
Modernize and fix Python graph examples with type hints, docstrings, and improvements
Co-authored-by: djeada <37275728+djeada@users.noreply.github.com>
1 parent 4f486da commit 26503ff

28 files changed

Lines changed: 847 additions & 528 deletions

File tree

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,73 @@
1+
"""A* search algorithm implementation."""
2+
3+
from __future__ import annotations
4+
5+
import math
16
from heapq import heappush, heappop
7+
from typing import Callable, Any
8+
9+
from graph import Graph, Vertex
210

311

4-
def a_star(graph, source, destination, heuristic):
12+
def a_star(
13+
graph: Graph,
14+
source: Vertex,
15+
destination: Vertex,
16+
heuristic: Callable[[Any, Any], float],
17+
) -> float:
18+
"""
19+
Find the shortest path between source and destination using A* algorithm.
520
21+
Args:
22+
graph: The graph to search.
23+
source: The starting vertex.
24+
destination: The target vertex.
25+
heuristic: A function that estimates the distance between two vertex values.
26+
27+
Returns:
28+
The shortest distance from source to destination, or infinity if no path exists.
29+
"""
630
if not (graph.contains(source) and graph.contains(destination)):
7-
return float("inf")
31+
return math.inf
832

9-
distance_from_source = dict()
10-
distance_till_destination = dict()
11-
# previous_vertex = dict()
33+
distance_from_source: dict[Vertex, float] = {}
34+
distance_till_destination: dict[Vertex, float] = {}
1235

1336
for vertex in graph.vertices():
14-
distance_from_source[vertex] = float("inf")
15-
distance_till_destination[vertex] = float("inf")
37+
distance_from_source[vertex] = math.inf
38+
distance_till_destination[vertex] = math.inf
1639

1740
distance_from_source[source] = 0
1841
distance_till_destination[source] = heuristic(source.value, destination.value)
1942

20-
visited = list()
21-
heappush(visited, (distance_till_destination[source], source))
22-
23-
while visited:
43+
open_set: list[tuple[float, Vertex]] = []
44+
heappush(open_set, (distance_till_destination[source], source))
45+
in_open_set: set[Vertex] = {source}
2446

25-
current = heappop(visited)[1]
47+
while open_set:
48+
current = heappop(open_set)[1]
49+
in_open_set.discard(current)
2650

2751
if current == destination:
2852
return distance_from_source[destination]
29-
# return reconstruct_path(previous_vertex, current)
3053

3154
for edge in graph.edges_from_vertex(current):
32-
3355
tentative_distance_from_source = (
3456
distance_from_source[current] + edge.distance
3557
)
3658

3759
if tentative_distance_from_source < distance_from_source[edge.destination]:
38-
# previous_vertex[edge.destination] = current
3960
distance_from_source[edge.destination] = tentative_distance_from_source
40-
distance_till_destination[edge.destination] = distance_from_source[
41-
edge.destination
42-
] + heuristic(edge.destination.value, destination.value)
61+
distance_till_destination[edge.destination] = (
62+
distance_from_source[edge.destination]
63+
+ heuristic(edge.destination.value, destination.value)
64+
)
4365

44-
if edge.destination not in visited:
66+
if edge.destination not in in_open_set:
4567
heappush(
46-
visited,
68+
open_set,
4769
(distance_till_destination[edge.destination], edge.destination),
4870
)
71+
in_open_set.add(edge.destination)
4972

50-
return float("inf")
73+
return math.inf
Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,102 @@
1+
"""Graph data structures for graph algorithms."""
2+
3+
from __future__ import annotations
4+
5+
import math
6+
from typing import Any, TypeVar
7+
8+
T = TypeVar("T")
9+
10+
111
class Vertex:
2-
def __init__(self, value):
3-
self.value = value
12+
"""A vertex in a graph with an associated value."""
413

5-
def __eq__(self, other):
14+
def __init__(self, value: T) -> None:
15+
self.value = value
616

17+
def __eq__(self, other: object) -> bool:
718
if isinstance(other, Vertex):
819
return self.value == other.value
9-
1020
return False
1121

12-
def __lt__(self, other):
13-
22+
def __lt__(self, other: object) -> bool:
1423
if not isinstance(other, Vertex):
1524
raise NotImplementedError
16-
1725
return self.value < other.value
1826

19-
def __repr__(self):
27+
def __repr__(self) -> str:
2028
return str(self.value)
2129

22-
def __hash__(self):
30+
def __hash__(self) -> int:
2331
return hash(self.value)
2432

2533

2634
class Edge:
27-
def __init__(self, source, destination, distance):
35+
"""A weighted directed edge connecting two vertices."""
36+
37+
def __init__(self, source: Vertex, destination: Vertex, distance: float) -> None:
2838
self.source = source
2939
self.destination = destination
3040
self.distance = distance
3141

32-
def __eq__(self, other):
33-
42+
def __eq__(self, other: object) -> bool:
3443
if isinstance(other, Edge):
3544
return (
3645
self.source == other.source
3746
and self.destination == other.destination
3847
and self.distance == other.distance
3948
)
40-
4149
return False
4250

43-
def __lt__(self, other):
44-
51+
def __lt__(self, other: object) -> bool:
4552
if not isinstance(other, Edge):
4653
raise NotImplementedError
47-
4854
return self.distance < other.distance
4955

50-
def __repr__(self):
56+
def __repr__(self) -> str:
5157
return f"({self.source}, {self.destination}, {self.distance})"
5258

5359

5460
class Graph:
55-
def __init__(self):
56-
self._adj_dict = dict()
61+
"""A directed weighted graph using adjacency list representation."""
5762

58-
def add_edge(self, edge):
63+
def __init__(self) -> None:
64+
self._adj_dict: dict[Vertex, list[Edge]] = {}
5965

66+
def add_edge(self, edge: Edge) -> None:
67+
"""Add an edge to the graph."""
6068
if edge.source in self._adj_dict:
6169
self._adj_dict[edge.source].append(edge)
62-
6370
else:
6471
self._adj_dict[edge.source] = [edge]
6572

6673
if edge.destination not in self._adj_dict:
67-
self._adj_dict[edge.destination] = list()
74+
self._adj_dict[edge.destination] = []
6875

69-
def add_vertex(self, vertex):
76+
def add_vertex(self, vertex: Vertex) -> None:
77+
"""Add a vertex to the graph."""
7078
if vertex not in self._adj_dict:
71-
self._adj_dict[vertex] = list()
79+
self._adj_dict[vertex] = []
7280

73-
def vertices(self):
81+
def vertices(self) -> list[Vertex]:
82+
"""Return a list of all vertices in the graph."""
7483
return list(self._adj_dict.keys())
7584

76-
def edges(self):
85+
def edges(self) -> list[Edge]:
86+
"""Return a list of all edges in the graph."""
7787
return [elem for array in self._adj_dict.values() for elem in array if array]
7888

79-
def edges_from_vertex(self, vertex):
89+
def edges_from_vertex(self, vertex: Vertex) -> list[Edge]:
90+
"""Return all edges originating from the given vertex."""
8091
self._assert_graph_contains_vertex(vertex)
8192
return self._adj_dict[vertex]
8293

83-
def contains(self, vertex):
94+
def contains(self, vertex: Vertex) -> bool:
95+
"""Check if the graph contains the given vertex."""
8496
return vertex in self._adj_dict
8597

86-
def connected(self, vertex_a, vertex_b):
98+
def connected(self, vertex_a: Vertex, vertex_b: Vertex) -> bool:
99+
"""Check if two vertices are directly connected by an edge."""
87100
self._assert_graph_contains_vertex(vertex_a)
88101
self._assert_graph_contains_vertex(vertex_b)
89102

@@ -97,23 +110,23 @@ def connected(self, vertex_a, vertex_b):
97110

98111
return False
99112

100-
def empty(self):
113+
def empty(self) -> bool:
114+
"""Check if the graph has no vertices."""
101115
return len(self._adj_dict) == 0
102116

103-
def size(self):
117+
def size(self) -> int:
118+
"""Return the number of vertices in the graph."""
104119
return len(self._adj_dict)
105120

106-
def __repr__(self):
107-
121+
def __repr__(self) -> str:
108122
result = ""
109123
for vertex, edges in self._adj_dict.items():
110124
result += f"{vertex} —> "
111125
for edge in edges:
112-
result += edge
126+
result += str(edge)
113127
result += "\n"
114-
115128
return result
116129

117-
def _assert_graph_contains_vertex(self, vertex):
130+
def _assert_graph_contains_vertex(self, vertex: Vertex) -> None:
118131
if vertex not in self._adj_dict:
119-
raise KeyError(f"Graph doesn't containt the {vertex} vertex!")
132+
raise KeyError(f"Graph doesn't contain the {vertex} vertex!")

src/graphs/python/a_star/tests/test_a_star.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ def distance(point_a, point_b):
4242
self.assertEqual(a_star(graph, Vertex(a), Vertex(c), Point.distance), 18)
4343
self.assertEqual(a_star(graph, Vertex(a), Vertex(b), Point.distance), 6)
4444
self.assertEqual(
45-
a_star(graph, Vertex(d), Vertex(b), Point.distance), float("inf")
45+
a_star(graph, Vertex(d), Vertex(b), Point.distance), math.inf
4646
)
4747
self.assertEqual(
48-
a_star(graph, Vertex(b), Vertex(e), Point.distance), float("inf")
48+
a_star(graph, Vertex(b), Vertex(e), Point.distance), math.inf
4949
)
5050

5151

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,56 @@
1-
from graph import Graph
1+
"""Bellman-Ford algorithm for shortest paths with negative edges."""
22

3+
from __future__ import annotations
34

4-
def bellman_ford(graph, source, destination):
5+
import math
56

7+
from graph import Graph, Vertex
8+
9+
10+
def bellman_ford(graph: Graph, source: Vertex, destination: Vertex) -> float:
11+
"""
12+
Find the shortest path between source and destination using Bellman-Ford algorithm.
13+
14+
This algorithm handles negative edge weights and detects negative cycles.
15+
16+
Args:
17+
graph: The graph to search.
18+
source: The starting vertex.
19+
destination: The target vertex.
20+
21+
Returns:
22+
The shortest distance from source to destination, or infinity if no path exists
23+
or if a negative cycle is detected.
24+
"""
625
if not (graph.contains(source) and graph.contains(destination)):
7-
return float("inf")
26+
return math.inf
827

928
if source == destination:
1029
return 0
1130

12-
distances = dict()
13-
visited = dict()
31+
distances: dict[Vertex, float] = {}
1432

1533
for vertex in graph.vertices():
16-
distances[vertex] = float("inf")
17-
visited[vertex] = False
34+
distances[vertex] = math.inf
1835

1936
distances[source] = 0
2037

21-
for i in range(graph.size() - 1):
38+
for _ in range(graph.size() - 1):
2239
for edge in graph.edges():
23-
2440
new_distance = distances[edge.source] + edge.distance
2541

2642
if (
27-
distances[edge.source] != float("Inf")
43+
distances[edge.source] != math.inf
2844
and new_distance < distances[edge.destination]
2945
):
3046
distances[edge.destination] = new_distance
3147

3248
for edge in graph.edges():
3349
if (
34-
distances[edge.source] != float("Inf")
50+
distances[edge.source] != math.inf
3551
and distances[edge.source] + edge.distance < distances[edge.destination]
3652
):
3753
# Graph contains negative weight cycle
38-
return float("inf")
54+
return math.inf
3955

4056
return distances[destination]

0 commit comments

Comments
 (0)