Protokół iteracyjny w Pythonie
Sat 27 January 2018 #technical #programming #pythonPython jest językiem, do którego mam bardzo ambiwalentny stosunek. Z jednej strony bardzo mi się podoba to, w jaki sposób się w nim pisze, z drugiej widzę w nim pewne założenia które zupełnie mi nie odpowiadają (powiedzmy, że nie jestem największym fanem programowania obiektowego). Nie można mu jednak zarzucić tego, że jest brzydki lub mało czytelny (co często ma miejsce w przypadku Perla, w którym również zdarza mi się programować).
Jedna z rzeczy, która w Pythonie mi się bardzo podoba to możliwość zdefiniowania, w jaki sposób będziemy iterować po klasie, którą sami tworzymy.
Weźmy na przykład strukturę, która nie jest liniowa, w tym przypadku niech to będzie graf:
# Przyjmijmy, że mamy graf który w przybliżeniu wygląda tak
#
# A E
# / \ /
# / \ /
# B-----C----D-----F
#
# Tłumacząc to na kod można by otrzymać taki zapis
g = Graph()
g.add(vertex='A', neighbours=['B', 'C'])
g.add(vertex='B', neighbours=['A', 'C'])
g.add(vertex='C', neighbours=['A', 'B', 'D'])
g.add(vertex='D', neighbours=['C', 'E', F'])
g.add(vertex='E', neighbours=['D'])
g.add(vertex='F', neighbours=['D'])
Są różne sposoby przechodzenia przez wszystkie wierzchołki grafu, na przykład przeszukiwanie wszerz (Breadth-First Search) czy przeszukiwanie w głąb (Depth-First Search) oba podejścia mają swoje zady i walety, są również podstawą dla innych bardziej złożonych algorytmów (np. przy rozwiązywaniu problemu maksymalnego przepływu) dlatego wspaniale by było napisać po prostu coś takiego:
for i,v in enumerate(g.bfs(start='A')):
print i, v
# skutkuje wypisaniem
# 0 A
# 1 B
# 2 C
# 3 D
# 4 E
# 5 F
a następnie skupić się na implementacji szczegółów innych rozwiązań
I Python nam na to pozwala!
Trzeba jedynie zdefiniować iterator, który będzie przechodzić przez wierzchołki grafu w odpowiedni sposób.
Pisanie iteratora
Po pierwsze, trzeba by zdefiniować klasę reprezentującą graf. Załóżmy, że mamy
już to zrobione (możemy mieć macierz albo listy przyległości). W takim wypadku
będziemy mieć zdefiniowaną (jak w przykładzie powyżej) następującą metodę
zwracającą iterator, który będzie przechodzić przez graf zaczynając od
konkretnego wierzchołka (start
):
def bfs(self, start=None):
# utwórz i zwróć obiekt iteratora
return _bfs_iterator(self, start_v=start)
Iterator musi przechowywać obecny stan iteracji (pozwala na odwołanie się do bierzącego elementu) oraz posiadać wiedzę o tym, w jaki sposób przejść do następnego elementu z kolekcji. Można powiedzieć, że jest to uogólnione pojęcie wskaźnika.
Spróbujmy to napisać zgodnie z wymaganiami Pythona, które wyglądają następujaco:
The iterator objects themselves are required to support the following two methods, which together form the iterator protocol:
iterator.__iter__()
Return the iterator object itself. This is required to allow both containers and iterators to be used with the
for
andin
statements. This method corresponds to the tp_iter slot of the type structure for Python objects in the Python/C API.
iterator.next()
Return the next item from the container. If there are no further items, raise the StopIteration exception. This method corresponds to the tp_iternext slot of the type structure for Python objects in the Python/C API.
Zdefiniujmy klasę następująco:
class _bfs_iterator():
def __init__(self, g, start_v):
self.graph = g
self.visited = {} # musimy zapisać gdzieś
# odwiedzone wierzchołki
self.fifo = collections.deque()
self.fifo.append(start_v) # dodajemy element od
# którego będziemy zaczynać
W celu zapisania, które wierzchołki odwiedziliśmy posłużę się tutaj słownikiem.
W ten sposób będziemy mogli skorzystać z szybszego dostępu do elementów
słownika, co przyspieszy wykonanie przy korzystaniu z pythonowego in
.
Zdefiniujmy, więc pierwszą metodę potrzebną do protokołu iteracyjnego Pythona (uwaga, brak wybuchów).
def __iter__(self):
return self
Teraz zaimplementujemy BFS w metodzie next
uwzględniając wymogi Pythona:
def next(self):
while len(self.fifo):
vertex = self.fifo.popleft()
if vertex not in self.visited:
# Zapamiętujemy wierzchołek, który odwiedzamy
self.visited[vertex] = True
# dodajemy jego sąsiadów
# jako kolejnych do odwiedzenia
for n in self.graph.adj_list[vertex]:
self.fifo.append(n)
break
else:
raise StopIteration()
return vertex
Kiedy dodajemy sąsiadów do kolejki odwołujemy się do wewnętrznej listy (dwu
wymiarowej) przyległości dla wierzchołka, który teraz odwiedzamy, w tym wypadku
nazwałem ją adj_list
. Równie dobrze mogłaby to być metoda, która zwraca
przylegające wierzchołki, gdyby implementacja grafu polegała na macierzy.
I w tym momencie skończyliśmy naszą pracę! Mamy iterator, dzięki któremu możemy
przejść po grafie przy użyciu wbudowanego, pythonowego for
a.
Zachęcam do napisania również DFSa w takiej formie jako ćwiczenie ;)
A także do przejrzenia mojej implementacji grafu na GitHubie, gdzie napisałem oba iteratory. Wszelkie uwagi do tekstu, jak i do samej implementacji mile widziane (jak zawsze zachęcam do skorzystania z poczty elektronicznej).