Jakiś czas temu podawałem sposób na sortowanie tabeli piłkarskiej Ekstraklasy w PHP, rozwiązanie działa znakomicie i jest bardzo łatwe w dostosowywaniu do własnych potrzeb. Od jakiegoś czasu zastanawiałem się czy da się osiągnąć podobny efekt z wykorzystaniem Pythona i Django, i jak to będzie się prezentowało od strony kodu…
O ile dla kogoś, kto programuje w Pythonie (nawet przy użyciu Django) od dłuższego czasu, stworzenie tabeli Polskiej Ekstraklasy nie powinno być problemem, to dla mnie problemem było, gdyż przygodę z Pythonem i Django rozpocząłem… ze trzy tygodnie temu.
Postawiłem sobie za cel stworzenie aplikacji, która będzie aktualizowała tabelę na podstawie dostarczanych przeze mnie „ręcznie” wyników spotkań. Jeśli chodzi o zwykłą tabelę piłkarską nie ma najmniejszego problemu. Wystarczy pobrać przykład ze strony bitbucket i już mamy (prawie) działającą tabelę. Brakuje w niej jednak opcji sortowania w przypadku równej różnicy bramek. Wystarczy dopisać jeden warunek i już działa. Schody zaczynają się w przypadku Polskiej Ekstraklasy…
Tutaj już nie tylko decyduje stosunek bramek, ale przy równej liczbie punktów, o kolejności w tabeli decydują spotkania bezpośrednie zainteresowanych drużyn, a w przypadku dwóch drużyn dochodzi warunek większej liczby goli strzelonych na wyjeździe. We wcześniejszym przykładzie pod PHP nakreśliłem jak dokonać uwzględnienia bezpośrednich spotkań.
W przypadku Pythona, jest to… łatwiejsze…
class TableEntry(object): """Represents a single entry in a league table.""" def __init__(self, name): self.name = name self.home_wins = self.home_draws = self.home_losses = 0 self.away_wins = self.away_draws = self.away_losses = 0 self.goals_for = self.goals_against = 0 def played(self): return (self.home_wins + self.home_draws + self.home_losses + self.away_wins + self.away_draws + self.away_losses) def points(self): return (3*(self.home_wins + self.away_wins) + self.home_draws + self.away_draws) def goal_difference(self): return self.goals_for - self.goals_against def __cmp__(self, other): if self.points() == other.points(): return cmp(other.goal_difference(), self.goal_difference()) else: return cmp(other.points(), self.points())
Tak wygląda oryginalna klasa sortująca tabelę z podanego przeze mnie przykładu…
Jak zatem widać wszystko jest tu przedstawione jasno i klarownie. Za sortowanie odpowiada funkcja __cmp__ i to ona jest kluczem do skumania o co w tym biega. Nie warto nawet opisywać tego dokładnie bo widać to gołym okiem.
Po dostosowaniu tej funkcji do moich potrzeb (a właściwie regulaminu Ekstraklasy) otrzymałem coś takiego:
def __cmp__(self, other): if self.points() == other.points(): teams = 0 self.match_between(other) if self.played_mb() > 1: if self.points_mb() == other.points_mb(): teams += 1 if self.goal_difference_mb() == other.goal_difference_mb(): if self.goals_fors_mb() == other.goals_fors_mb(): if teams == 2 & self.played_mb() == 2: return cmp(other.goals_aways(),self.goals_aways()) else: cmp(other.goals_fors_mb(),self.goals_fors_mb()) else: cmp(other.goal_difference_mb(),self.goal_difference_mb()) else: cmp(other.points_mb(),self.points_mb()) else: if self.goal_difference() == other.goal_difference(): return cmp(other.goals_fors(),self.goals_fors()) else: return cmp(other.goal_difference(),self.goal_difference()) else: return cmp(other.points(), self.points())
Widać, że pojawia się kilka innych rzeczy, odwołania do funkcji zakończone _mb dotyczą bezpośrednich spotkań, natomiast
self.match_between(other)
odwołuje się do funkcji, która pobiera bezpośrednie spotkania pomiędzy dwoma dużynami
def match_between(self, other): try: h = Matches.objects.get(played=1,home_team=self.id,away_team=other.id) except Matches.DoesNotExist: h = False if h: self.goals_for_mb += h.home_score self.goals_against_mb += h.away_score other.goals_for_mb += h.away_score other.goals_against_mb += h.home_score other.goals_away += h.away_score if h.home_score > h.away_score: self.home_wins_mb += 1 other.away_losses_mb += 1 elif h.home_score == h.away_score: self.home_draws_mb += 1 other.away_draws_mb += 1 elif h.home_score < h.away_score: self.home_losses_mb += 1 other.away_wins_mb += 1 try: a = Matches.objects.get(played=1,away_team=self.id,home_team=other.id) except Matches.DoesNotExist: a = False if a: self.goals_for_mb += a.away_score self.goals_against_mb += a.home_score self.goals_away += a.away_score other.goals_for_mb += a.home_score other.goals_against_mb += a.away_score if a.home_score < a.away_score: self.away_wins_mb += 1 other.home_losses_mb += 1 elif a.home_score == a.away_score: self.away_draws_mb += 1 other.home_draws_mb += 1 elif a.home_score > a.away_score: self.away_losses_mb += 1 other.home_wins_mb += 1
Funkcja od spotkań bezpośrednich niczego nie zwraca, a jedynie… dokonuje stosownych obliczeń, które będą wykorzystane w funkcji __cmp__, a także w innych, które będą się do tych danych odwoływać.
Z kolei warunek
if teams == 2 & self.played_mb() == 2:
Sprawdza czy są dwie drużyny z tą samą ilością punktów, a także czy rozegrały ze sobą po dwa spotkania (mecz i rewanż), jeśli warunek będzie spełniony, sortowanie nastąpić ma (w przypadku równej różnicy bramek oraz równej ilości strzelonych bramek) po bramkach zdobytych na wyjeździe.
W porównaniu z PHP gołym okiem widać, że nie ma tu żadnych pętli (choćby od pobierania wyników spotkań), nie ma trzech razy odwołań do funkcji array_multisort, odpowiedzialnej za sortowanie, gdyż za wszystko odpowiada jedna funkcja __cmp__, która przekazuje to, co powinno być brane pod uwagę przy sortowaniu; i dlatego w klasie Tabulator wystarczy funkcja
def table(self): return sorted(self.data.values())
która zwróci nam ładnie ułożoną tabelą uwzględniającą punkty, bezpośrednie spotkania, różnicę bramek i ilość bramek strzelonych.

Zrozumienie tego, że w Pythonie wszystko jest proste i nie wymaga nadmiernego kombinowania zajęło mi dość dużo czasu. W efekcie pojąłem o ile szybciej tworzy się rozbudowane serwisy z wykorzystaniem Django, i o ile kod prezentuje się „ładniej”.
Tym, których przeraża ogrom możliwości Pythona, zachęcam do skorzystania z frameworka Django, który nie tylko ułatwi stworzenie rozbudowanych serwisów, ale także znacząco skróci czas ich wykonania.


