input: Write a python function to implement A* search algorithm.
  assert AStar('A', expand_edge, heuristic_cost, goal_test).search() == ['A', 'B', 'G']
  assert AStar('S', expand_edge, heuristic_cost, goal_test).search() == ['S', 'A', 'G']
  assert AStar('X', expand_edge, heuristic_cost, goal_test).search() == ['X', 'Y', 'G']
output: import heapq
  
  class AStar:
      def __init__(self, start, expand_edge, heuristic_cost, goal_test):
          self.start = start
          self.expand_edge = expand_edge
          self.heuristic_cost = heuristic_cost
          self.goal_test = goal_test
  
      def search(self):
          open_set = []
          heapq.heappush(open_set, (0, self.start))
          came_from = {self.start: None}
          g_score = {self.start: 0}
  
          while open_set:
              current_cost, current_node = heapq.heappop(open_set)
              
              if self.goal_test(current_node):
                  return self.reconstruct_path(came_from, current_node)
  
              for neighbor, cost in self.expand_edge(current_node):
                  tentative_g_score = g_score[current_node] + cost
                  if neighbor not in g_score or tentative_g_score < g_score[neighbor]:
                      came_from[neighbor] = current_node
                      g_score[neighbor] = tentative_g_score
                      f_score = tentative_g_score + self.heuristic_cost(neighbor)
                      heapq.heappush(open_set, (f_score, neighbor))
          
          return None
  
      def reconstruct_path(self, came_from, current):
          path = []
          while current is not None:
              path.append(current)
              current = came_from[current]
          path.reverse()
          return path