31 Sierpień 2011

Tabela Ekstraklasy w Django…

djangoJakiś 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.

ekstraklasa w Django

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.