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:

  1. Tworzy tablicę od 1 do R (⍳R)
  2. Odrzuca jedynkę i przypisuje wynik do R (R←1↓)
  3. Tworzy tablicę mnożenia z dwóch tablic R (R∘.×R)
  4. Wyznacza, które liczby z R należą do tablicy mnożenia, tworzy tablicę zer (nie należy) i jedynek (należy) (∊)
  5. Neguje tę tablicę (~). Uzyskana tablica zawiera pozycję wszystkich liczb pierwszych.
  6. 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.

Meta

Published: Lis 17, 2010

Author: zefciu

Comments:  

Word Count: 1,022

Next: Dekoratory część 2

Previous: Dekoratory część 1

Bookmark and Share

Tags

apl ezoteryka

Follow-Up Articles

Article Links

  1. Nie znaleziono bloga
  2. Eternal Flame
  3. 400 Bad Request
Comments powered by Disqus