Wyginanie pythona
Dzisiejszy wpis będzie miał charakter zabawowo-ezoteryczny. Istnieją na tym świecie języki programowania, których używa niewiele osób, lecz ci którzy je używają nie potrafią wyjść z podziwu nad nimi, zwykle zachwalając jakąś ich cechę. Dziś zajmiemy się językiem APL. Ponoć system kontroli lotów w tym języku zajmuje 5 linijek. Jego entuzjaści przechwalają się jednolinijkowymi (i totalnie niezrozumiałymi dla kogoś kto zna "normalne" języki) implementacjami trudnych algorytmów. Mniej entuzjastyczni nazywają go językiem dla marsjan, lub odmawiają użycia go z nienawiści dla hieroglifów
A my będziemy znęcać się nad Pythonem
My jednak nie będziemy tutaj zajmować się flame'owaniem. Podobno są programiści, którzy w każdym języku umieją pisać w FORTRANie. My napiszemy w Pythonie program w APL-u. Do zabawy użyjemy następującego kodu:
(~R∊R∘.×R)/R←1↓⍳R
Pozwoliłem go sobie zaczerpnąć z artykułu na wikipedii. Kto nie wierzy, że jest to naprawdę działające sito Eratostenesa, niech zainstaluje sobie np. A+ FSF i sprawdzi. Program działa następująco:
- Tworzy tablicę od 1 do R (⍳R)
- Odrzuca jedynkę i przypisuje wynik do R (R←1↓)
- Tworzy tablicę mnożenia z dwóch tablic R (R∘.×R)
- Wyznacza, które liczby z R należą do tablicy mnożenia, tworzy tablicę zer (nie należy) i jedynek (należy) (∊)
- Neguje tę tablicę (~). Uzyskana tablica zawiera pozycję wszystkich liczb pierwszych.
- Powtarza liczby z tablicy R tyle razy, ile wynosi odpowiadająca im liczba w powyższej tablicy (0 lub 1 raz) (/)
Zaczynamy budować nasz obiekt
Skoro chcemy uzyskać efekty podobne jak w APL-u, musimy stworzyć klasę obiektów, które będą się zachowywać podobnie, jak obiekty z tamtego języka. Od razu zaznaczam, że będziemy grzeszyć tu solidnie przeciwko zasadom wskazującym jak powinny zachowywać się tworzone przez programistę kontenery pythona. W końcu nie do pythonicznego zachowania dążymy. Niech nam duchy programowania wybaczą.
class Apl(object): def __init__(self, list_, depth = 1): self.list_ = list_ self.depth = depth
Tworzymy tutaj obiekt, który zawiera pythonową listę, a nie dziedziczy po niej. Powodem takiego wyboru, oprócz chęci zachowania prostoty jest to, że definiując nasze bardzo niestandardowe metody moglibyścy zasłonić metody wbudowane. To z kolei prowadzić może do pojawienia się nieskończonej rekurencji w nieoczekiwanych miejscach. Dodatkowo klasa Apl przechowuje informację o swojej głębokości. Nie jest to w żaden sposób sprawdzane. Jeśli podamy listę zagnieżdżoną, ale depth = 1, obiekt będzie się zachowywał jak tablica jednowymiarowa zawierająca listy.
Alternatywny konstruktor
Niektóre klasy biblioteki standardowej zawierają alternatywne konstruktory. Przykładem może być metoda Date.today() z modułu datetime. My chcemy uzyskać coś podobnego do operatora ⍳ z języka APL. Dlatego stwórzmy sobie metodę:
@classmethod def range(cls, *args): return cls(range(*args), 1)
Jest to metoda statyczna. Będzie ona wywoływana na klasie, a nie na jej instancjach. Nie przyjmuje więc argumentu self. Zamiast niego mamy argument cls, w którym zostanie przekazana klasa, z której metoda została wywołana. O przekazanie tego argumentu zadba zaś dekorator @classmethod. Czy moglibyśmy darować sobie dekorowanie i po prostu kazać metodzie stworzyć explicite obiekt klasy Apl? Moglibyśmy, ale byłoby to złe rozwiązania ze względu na dziedziczenie. We wszystkich klasach potomnych konstruktor tworzyłby instancję klasy macierzystej. Dlatego dekorator @classmethod jest godny zapamiętania jako wygodny i czytelny sposób tworzenia metod statycznych.
Grzechy ciężkie
Ostatnim elementem jest przeciążenie operatorów, które w pythonie sprowadza się do zdefiniowania pewnych metod specjalnych:
def __str__(self): if self.depth in [2, 3]: return ('\n' * (self.depth - 1)).join(str(Apl(x)) for x in self.list_) elif self.depth == 1: return '\t'.join(str(x) for x in self.list_) else: return '<Apl object with depth of %i' % self.depth __repr__ = __str__ def __mul__(self, other): return Apl([[x * y for y in other.list_] for x in self.list_], self.depth + other.depth) def __and__ (self, item): return self.map(item.__contains__) def __contains__(self, item): if self.depth == 1: return item in self.list_ else: for el in self.list_: if item in Apl(el): return True return False def __invert__(self): return self.map (lambda x: not x) def map(self, f): if self.depth == 1: return Apl([f(x) for x in self.list_]) else: return Apl([Apl(x, self.depth - 1).map(f).list_ for x in self.list_]) def __div__(self, other): result = [] for i in range(len(other.list_)): result += [other.list_[i]] * self.list_[i] return Apl(result, 1)
Metoda __mul__ odpowiadająca operatorowi * zastąpi nam ∘.× i wyliczy tabliczkę mnożenia, __invert__ to operator bitowy ~, zaś __div__ to operator dzielenia. Będą udawały operatory o tym samym kształcie. Operator bitowy & zastąpi nam ∊. Przyznaję, że myślałem o użyciu __contains__ (przeciąża działanie słowa kluczowego in), ale interpreter Pythona konwertuje wynik zwracany przez tę metodę do typu bool. No cóż. Postawiono pewną granicę możliwości psucia.
Dosyć ochoczo korzystamy w powyższym kodzie z rekurencji. Ponieważ może to być groźne, musimy zapytać się siebie, czy mamy gwarancję, że rekurencja szybko się skończy. Tutaj głębokość rekurencji zależy od tego, jakich tablic użyjemy. Że nie będziemy operować tablicami głębszymi niż 2-wymiarowe, rekurencja jest bezpieczna.
Sprawdźmy
Skoro zdefiniowaliśmy już wszystko, co było nam potrzebne, spróbujmy użyć naszej klasy do napisania APL-owego z początku wpisu:
a = Apl.range(2, 100) print(~(a & (a * a)) / a)
Pięknie! Program działa, jest tak samo zwięzły i nieczytelny jak program napisany w APL i jego zasada funkcjonowania jest taka sama. Podobnie jak APL pracuje na tablicach. Moglibyśmy myśleć o zamianie tablic na iteratory, ale kosztowałoby to nas wydajność, gdyż nasza tabliczka mnożenia nie byłaby zapisana w pamięci, tylko musiałaby być na nowo liczona z przy każdym teście przynależności.
I po co nam to było?
Oczywiście normalny pythonista napisze np. tak:
primes = [] for i in range(2, 100): for p in primes: if not i % p: break else: primes.append(i) print primes
Program jest prosty, czytelny, działa przynajmniej dwa razy szybciej niż tamten, a jeszcze możemy go optymalizować. Cała nasza dzisiejsza przygoda miała na celu pokazanie możliwości, jakie oferuje nam język Python, a także przypomnienie sobie o kilku zasadach i technikach przydatnych w tworzeniu nowych klas. W następnym wpisie wracamy do dekoratorów i rzeczy praktycznych i pożytecznych.