Obsah
1. Knihovny pro zpracování posloupností (sekvencí) v Pythonu
2. Sekvence a lazy sekvence v programovacím jazyku Clojure
3. Sekvence a lazy sekvence v Pythonu a knihovně clj
5. Základní funkce pro práci se sekvencemi
8. Druhý demonstrační příklad – použití funkcí filter a remove
10. Třetí příklad – kombinace remove a take-while pro nekonečné sekvence
11. Přístup k prvkům nekonečné sekvence
12. Funkce nth a take v Pythonu
15. Rozdělení sekvence do podskupin s využitím funkce group-by
17. Sloučení několika sekvencí funkcí interleave
18. Funkce interleave v Pythonu
19. Repositář s demonstračními příklady
1. Knihovny pro zpracování posloupností (sekvencí) v Pythonu
V seriálu o programovacím jazyce Clojure jsme se již mnohokrát setkali s pojmem sekvence, popř. nekonečné sekvence nebo dokonce lazy sekvence. Jedná se o datovou abstrakci, která je sice velmi jednoduchá, ale o to užitečnější v praxi – ostatně velká část standardní knihovny Clojure je na sekvencích založena. Pro ty programátory, kteří programovací jazyk Clojure znají a současně používají i Python, je určena minimalisticky pojatá knihovna nazvaná clj. V této knihovně nalezneme implementaci všech základních funkcí, které jsou v Clojure určeny pro práci se sekvencemi. Tyto funkce je možné použít i pro klasické seznamy a iterátory, jak ostatně uvidíme v dalším textu.
Abychom pochopili, jaké funkce a generátory nalezneme v knihovně clj, popišme si nejprve ve stručnosti sekvence a lazy sekvence tak, jak jsou implementovány přímo v programovacím jazyce Clojure. Posléze se podíváme na to, do jaké míry byl úspěšný převod tohoto konceptu do Pythonu.
2. Sekvence a lazy sekvence v programovacím jazyku Clojure
Mnoho funkcí a maker, které nalezneme ve standardní knihovně programovacího jazyka Clojure, souvisí s takzvanými sekvencemi. Tímto termínem se označuje programové rozhraní, které svými základními možnostmi zhruba odpovídá iterátorům známým z programovacího jazyka Java. V Clojure existuje velké množství funkcí, které dokážou pracovat se sekvencemi, ať již se jedná o běžné sekvence (jejichž prvky jsou přímo uloženy v operační paměti) nebo takzvané líné sekvence (lazy sekvence), které nové prvky vytváří či zjišťují až při konkrétním přístupu na tyto prvky. Mezi tyto funkce patří například sort, sort-by, take či flatten. Díky tomu, že všechny standardní kolekce (seznamy, vektory, …) jsou současně i sekvencemi, lze tyto funkce aplikovat i na kolekce, ovšem ve skutečnosti jsou sekvencemi i další typy objektů, zejména pak I/O proudy (tímto směrem se posunuly i standardní knihovny Javy), řetězce (což jsou sekvence znaků) atd.
Naprostý základ pro práci se sekvencemi tvoří trojice funkcí nazvaných first, rest a next. Funkce first vrací první prvek v sekvenci, popř. speciální hodnotu nil v případě, že je sekvence prázdná. Funkce rest i next vrací zbylé prvky v sekvenci, ovšem liší se tím, jaká hodnota se vrátí ve chvíli, kdy již v sekvenci nezbyly žádné prvky (kromě prvního). V tomto případě vrátí rest prázdnou sekvenci (například prázdný seznam), zatímco funkce next vrátí již zmíněnou speciální hodnotu nil. U běžných sekvencí, například seznamů, jsou tyto funkce implementovány přímočaře, ovšem v případě lazy sekvencí se prvky vrácené pomocí funkce first vyhodnocují až za běhu, například pomocí nějaké generátorové funkce. Tímto způsobem je možné pracovat i s nekonečnými sekvencemi, u nichž už z principu nelze dopředu znát celkový počet prvků atd.
Velmi dobrým příkladem lazy sekvence je funkce range, která dokonce existuje v několika podobách, jež se od sebe z hlediska programátora-uživatele liší především různým počtem parametrů. Pokud se této funkci nepředá žádný parametr, vrátí funkce range sekvenci celých čísel od nuly do nekonečna. Zde je patrné, proč se musí jednat o lazy sekvenci – nekonečnou řadu celých čísel by samozřejmě v případě normální sekvence nebylo možné uložit do operační paměti. Pokud se funkci range předá pouze jediný parametr (kterým musí být celé číslo – je kontrolováno v runtime), je vrácena sekvence celých čísel od 0 do zadané hodnoty-1. Opět se jedná o nefalšovanou lazy sekvenci, takže se nemusíte bát používat i velké n. Dále již následují v podstatě jen kosmetické úpravy – volání funkce range se dvěma parametry m, n vytvoří sekvenci celých čísel od m do n-1 a pokud je použit ještě třetí parametr, určuje se jím krok, který může být i záporný.
Takto navrženou funkci range nalezneme i v knihovně clj. Je přitom zachováno standardní chování range ze základní knihovny Pythonu, ovšem v případě potřeby může tato funkce (volaná bez parametrů) vytvořit nekonečnou lazy sekvenci!
3. Sekvence a lazy sekvence v Pythonu a knihovně clj
V Pythonu se chování skutečných sekvencí a lazy sekvencí může napodobit pomocí generátorů. Ty byly původně do Pythonu přidány mj. i z důvodu kratšího a přehlednějšího zápisu iterátorů. U generátorů se nemusí psát celá třída s metodami __iter__() a __next__() atd.) a celý zápis generátoru je většinou realizován jedinou funkcí, v níž se nová hodnota generuje příkazem yield (současně se předává řízení koprogramu, který generátor používá). Stav generátoru je typicky uložen právě v této funkci v lokálních proměnných (což je na druhou stranu odlišné od chování jazyka Clojure a vede k nepříjemným vedlejším efektům).
Příkladem použití generátoru může být funkce range, která sice vychází ze standardní pythonovské funkce téhož jména, ovšem přidává možnost tvorby nekonečné sekvence:
def range(*args): """ Usage: range() range(end) range(start, end) range(start, end, step) Returns a generator of numbers from ``start`` (inclusive) to ``end`` (exclusive), by ``step``, where ``start`` defaults to ``0``, ``step`` to ``1``, and ``end`` to infinity. When ``step`` is equal to ``0``, returns an infinite sequence of ``start``. Note that this delegates to Python’s built-in ``range`` (or ``xrange`` in Python 2) if there are arguments. """ if args: for e in _range(*args): yield e return n = 0 while True: yield n n += 1
Samotná implementace nekonečné sekvence je zapsána v posledních čtyřech řádcích.
Naproti tomu v programovacím jazyku Clojure je podobná sekvence interně vytvářena takto (což je do značné míry totožné s implementací iterátoru v Pythonu):
private class RangeIterator implements Iterator { private Object next; public RangeIterator() { this.next = start; } public boolean hasNext() { return(! boundsCheck.exceededBounds(next)); } public Object next() { if (hasNext()) { Object ret = next; next = Numbers.addP(next, step); return ret; } else { throw new NoSuchElementException(); } } public void remove() { throw new UnsupportedOperationException(); } }
Ve skutečnosti se chování sekvencí v Clojure a Pythonu odlišuje, a to ve chvíli, kdy nějaká funkce musí „zkonzumovat“ data z generátoru, například při přístupu k n-tému prvku. V Clojure získáme vždy stejný (desátý) prvek nekonečné sekvence:
(def s (range)) (def i1 (nth s 10)) (def i2 (nth s 10)) (def i3 (nth s 10)) (println i1 i2 i3) 10 10 10
V Pythonu se ovšem používá společný stav generátoru a dostame výsledky rozdílné:
from clj.seqs import range, nth s = range() i1 = nth(s, 10) i2 = nth(s, 10) i3 = nth(s, 10) print(i1, i2, i3) 10 21 32
4. Instalace knihovny clj
Po krátkém úvodu nastal čas si vyzkoušet některé možnosti nabízené knihovnou clj. Samotná instalace této knihovny je velmi snadná, ostatně jedná se vlastně o pouhé dva zdrojové soubory. Vzhledem k tomu, že clj je dostupná na PyPi, můžeme pro instalaci použít nástroj pip:
$ pip3 install --user clj Collecting clj Downloading https://files.pythonhosted.org/packages/46/de/6d06743f2327f070602eb1f6dff525c92397de17fb418e206b13945e8468/clj-0.1.0.tar.gz Installing collected packages: clj Running setup.py install for clj ... done Successfully installed clj-0.1.0
Od této chvíle by mělo být možné provést například:
from clj.seqs import range, first, rest help("clj.seqs.first") Help on function first in clj.seqs: clj.seqs.first = first(coll) Returns the first item in the collection. If ``coll`` is ``None`` or empty, returns ``None``. help("clj.seqs.range") Help on function range in clj.seqs: clj.seqs.range = range(*args) Usage: range() range(end) range(start, end) range(start, end, step) Returns a generator of numbers from ``start`` (inclusive) to ``end`` (exclusive), by ``step``, where ``start`` defaults to ``0``, ``step`` to ``1``, and ``end`` to infinity. When ``step`` is equal to ``0``, returns an infinite sequence of ``start``. Note that this delegates to Python’s built-in ``range`` (or ``xrange`` in Python 2) if there are arguments.
5. Základní funkce pro práci se sekvencemi
Připomeňme si, že sekvence jsou v programovacím jazyce Clojure důležité především z toho důvodu, že ve standardní knihovně tohoto jazyka se nachází velké množství funkcí a maker pro práci s nimi. Tvůrci LISPu a z něho odvozeného jazyka Clojure totiž zastávají názor, že je lepší používat relativně malé množství datových typů a mít pro tyto typy k dispozici velké množství obecných funkcí, které je možné vzájemně kombinovat. Naproti tomu se v klasickém OOP spíše upřednostňuje mít větší počet specializovaných datových typů (tříd) s relativně malým množstvím specializovaných funkcí aplikovatelných na tyto typy (metody). Obecně asi nelze říci, který přístup je lepší, protože záleží na povaze řešené úlohy. Vraťme se však k jazyku Clojure a k jeho kolekcím. V následující tabulce jsou vypsány některé zcela základní funkce, které lze při práci s kolekcemi použít. Povšimněte si, že žádná z těchto funkcí nemění původní kolekci, maximálně vrátí jako svůj výsledek kolekci novou, která ve většině případů používá stejné prvky jako kolekce původní (tím se zamezuje zbytečným kopiím v paměti):
# | Funkce | Podpora v clj | Stručný popis funkce |
---|---|---|---|
1 | count | ano | vrátí počet prvků v sekvenci |
2 | cons | ano | vrátí novou kolekci s přidaným prvkem (odkaz jazyka LISP) |
3 | concat | ano | spojení dvou sekvencí (rozdílné od cons!) |
4 | first | ano | první prvek sekvence |
5 | second | ano | druhý prvek sekvence |
6 | rest | ano | sekvence bez prvního prvku |
7 | nth | ano | získání n-tého prvku sekvence |
8 | distinct | ano | vrátí se nová sekvence s unikátními prvky (bez duplicit) |
6. První demonstrační příklad – použití základních funkcí pro konstrukci sekvencí a pro přístup k prvkům sekvence
V prvním demonstračním příkladu si popíšeme způsob použití většiny funkcí popsaných v předchozí kapitole. Nejprve si ukážeme původní tvar příkladu naprogramovaného v jazyce Clojure. Zde jsou sekvence základním abstraktním typem; konkrétním typem může být například vektor:
(def s [1 2 3 1 2 3]) (println "Count:") (println (count s)) (println) (println "Reversed:") (println (reverse s)) (println "First:") (println (first s)) (println "Second:") (println (second s)) (println "Rest:") (println (rest s)) (println "Distinct items:") (println (distinct s)) (println "Cons 1:") (def new_sequence (cons s ["A" "B" "C"])) (println new_sequence) (println "Cons 2:") (def new_sequence_2 (cons ["A" "B" "C"] s)) (println new_sequence_2) (println "Concat 1:") (def new_sequence (concat s ["A" "B" "C"])) (println new_sequence) (println "Concat 2:") (def new_sequence_2 (concat ["A" "B" "C"] s)) (println new_sequence_2)
Výsledky tohoto příkladu po jeho spuštění v REPLu:
Count: 6 Reversed: (3 2 1 3 2 1) First: 1 Second: 2 Rest: (2 3 1 2 3) Distinct items: (1 2 3) Cons 1: ([1 2 3 1 2 3] A B C) Cons 2: ([A B C] 1 2 3 1 2 3) Concat 1: (1 2 3 1 2 3 A B C) Concat 2: (A B C 1 2 3 1 2 3)
Nyní se pokusme tento příklad převést do Pythonu. Na začátku musíme naimportovat všechny používané funkce:
from clj.seqs import count, first, second, rest, cons, concat, distinct
Dále si vytvoříme pomocné funkce pro tisk sekvence na standardní výstup:
def hr(): print(40*"-") print() def print_sequence(sequence): for item in sequence: print(item) hr()
A můžeme začít s přepisováním. Povšimněte si například toho, že namísto reverse je nutné použít standardní funkci reversed:
sequence = [1, 2, 3, 1, 2, 3] print("Original:") print_sequence(sequence) # puvodni funkce len() a nova funkce count() print("Len and count:") print(len(sequence)) print(count(sequence)) hr() # puvodni (standardni) funkce reversed() print("Reversed:") print_sequence(reversed(sequence)) # funkce first(), second() a rest() print("First:") print(first(sequence)) print("Second:") print(second(sequence)) print("Rest:") print(rest(sequence)) print_sequence(rest(sequence)) # nova funkce distinct() print("Distinct items:") print(distinct(sequence)) print_sequence(distinct(sequence)) # prevod sekvence zpet na seznam print(list(distinct(sequence))) # funkce cons print("Cons 1:") new_sequence = cons(sequence, ["A", "B", "C"]) print_sequence(new_sequence) print("Cons 2:") new_sequence_2 = cons(["A", "B", "C"], sequence) print_sequence(new_sequence_2) # funkce concat print("Concat 1:") new_sequence = concat(sequence, ["A", "B", "C"]) print_sequence(new_sequence) print("Concat 2:") new_sequence_2 = concat(["A", "B", "C"], sequence) print_sequence(new_sequence_2)
Výsledky by měly vypadat přesně takto:
Original: 1 2 3 1 2 3 ---------------------------------------- Len and count: 6 6 ---------------------------------------- Reversed: 3 2 1 3 2 1 ---------------------------------------- First: 1 Second: 2 Rest: <generator object drop at 0x7f255594b678> 2 3 1 2 3 ---------------------------------------- Distinct items: <generator object distinct at 0x7f255594b678> 1 2 3 ---------------------------------------- [1, 2, 3] Cons 1: [1, 2, 3, 1, 2, 3] A B C ---------------------------------------- Cons 2: ['A', 'B', 'C'] 1 2 3 1 2 3 ---------------------------------------- Concat 1: 1 2 3 1 2 3 A B C ---------------------------------------- Concat 2: A B C 1 2 3 1 2 3 ----------------------------------------
7. Funkce filter a remove
Velmi často se při zpracování sekvencí používají funkce nazvané filter a remove. Ve skutečnosti je funkce filter součástí standardní knihovny Pythonu, takže v knihovně clj musela být implementována pouze funkce remove. Obě funkce, tj. jak filter, tak i remove, jsou funkcemi vyššího řádu, což znamená, že jejich parametrem je další funkce (typicky funkce anonymní). Funkce filter se používá pro vytvoření nové sekvence, která bude obsahovat jen ty prvky ze sekvence původní, které odpovídají nějakému predikátu (predikát je obecně funkce vracející pro svůj jediný vstup pravdivostní hodnotu). Funkce remove pracuje přesně naopak, tj. odstraňuje ze sekvence ty prvky, které odpovídají predikátu.
Vzhledem k tomu, že knihovna clj realizuje sekvence formou generátorů, je implementace funkce remove značně strohá:
def remove(pred, coll): """ Return a generator of the items in ``coll`` for which ``pred(item)`` returns a falsy value. """ for e in coll: if not pred(e): yield e
8. Druhý demonstrační příklad – použití funkcí filter a remove
Ve druhém demonstračním příkladu si popíšeme chování funkcí filter a remove. Opět si nejprve uvedeme variantu určenou pro programovací jazyk Clojure. Ze sekvence s původně devíti prvky 1..9 nejprve vytvoříme sekvenci se všemi prvky dělitelnými třemi, posléze sekvenci se všemi prvky NEdělitelnými třemi, dále sekvenci získanou filtrací prvků, pro něž predikát vždy vrátí pravdivostní hodnotu True a konečně sekvenci získanou odstraněním prvků, pro něž predikát vždy vrací True:
(def s (range 1 10)) ; kratky zapis anonymni funkce pomoci #() (def filtered (filter #(zero? (mod % 3)) s)) (println filtered) ; kratky zapis anonymni funkce pomoci #() (def removed (remove #(zero? (mod % 3)) s)) (println removed) ; zde nelze kratky zapis pouzit - musi se specifikovat argument (def filtered (filter (fn [x] true) s)) (println filtered) ; zde nelze kratky zapis pouzit - musi se specifikovat argument (def removed (remove (fn [x] true) s)) (println removed)
Výsledky postupně vypadají takto:
; prvky delitelne tremi (3 6 9) ; prvky nedelitelne tremi (1 2 4 5 7 8) ; prvky filtrovane s vyuzitim predikatu, ktery vraci True (1 2 3 4 5 6 7 8 9) ; prvky odstranene s vyuzitim predikatu, ktery vraci True ()
Přepis do Pythonu je přímočarý a použijeme v něm anonymní funkce vytvářené pomocí konstrukce lambda:
from clj.seqs import remove def hr(): print(40*"-") print() def print_sequence(sequence): for item in sequence: print(item) hr() sequence = range(1, 10) print("Original:") print_sequence(sequence) filtered = filter(lambda x: x % 3 == 0, sequence) print("Filtered:") print_sequence(filtered) removed = remove(lambda x: x % 3 == 0, sequence) print("Removed:") print_sequence(removed) filtered = filter(lambda _: True, sequence) print("Filtered:") print_sequence(filtered) removed = remove(lambda _: True, sequence) print("Removed:") print_sequence(removed)
Výsledky odpovídají Clojure:
Original: 1 2 3 4 5 6 7 8 9 ---------------------------------------- Filtered: 3 6 9 ---------------------------------------- Removed: 1 2 4 5 7 8 ---------------------------------------- Filtered: 1 2 3 4 5 6 7 8 9 ---------------------------------------- Removed: (zde nic není :-) ----------------------------------------
9. Funkce take-while
V některých případech nám bude užitečná i poněkud komplikovanější funkce, která je nazvaná take-while. Zatímco u funkce take popsané dále se přímo zadává počet prvků lazy sekvence, která se má vrátit, je v případě funkce take-while namísto konstantního počtu prvků výsledné sekvence předán predikát, tj. funkce s (v tomto případě) jedním parametrem, jejímž výsledkem by měla být pravdivostní hodnota true nebo false.
Návratovou hodnotou funkce take-while je (obecně) opět lazy sekvence získaná ze vstupní sekvence, ovšem vráceno je pouze prvních n prvků, pro něž predikát vrací hodnotu true. Nejedná se však o klasický filtr (viz též předchozí dvě kapitoly), protože ihned ve chvíli, kdy predikát poprvé vrátí hodnotu false, je lazy sekvence ukončena. Pokud vrátí predikát hodnotu false již při prvním volání, je výsledkem prázdná sekvence, pokud naopak vrací hodnotu true vždy, vrátí se potenciálně nekonečná lazy sekvence (což však někdy nemusí vadit, pokud se tedy nebudeme snažit o výpis všech prvků). Vzhledem k určitým omezením take-while je nutné, aby měl predikát pouze jeden parametr, což většinou znamená, že si musíme vypomoci novou funkcí (popř. anonymní funkcí).
Příklad použití funkce take-while v Clojure:
(def my-sequence (range)) ; prvni pouziti nekonecne sekvence (println (take-while #(< % 10) my-sequence)) ; druhe pouziti nekonecne sekvence (println (take-while #(< % 10) my-sequence)) ; false se vrati ihned v prvnim volani (println (take-while (fn [x] false) my-sequence))
10. Třetí příklad – kombinace remove a take-while pro nekonečné sekvence
Výše zmíněné funkce remove a take-while nyní použijeme v Pythonu, a to vlastně naprosto stejným způsobem, jako tomu bylo v programovacím jazyku Clojure:
from clj.seqs import count, range, take_while, remove def hr(): print(40*"-") print() def print_sequence(sequence): for item in sequence: print(item) hr() sequence = range() # toto NE: print(count(sequence)) print("Vyber prvku mensich nez 10 ze vsech prirozenych cisel:") new_sequence_1 = take_while(lambda x: x < 10, sequence) print_sequence(new_sequence_1) def podminka(x): return x < 10 sequence = range() print("Vyber prvku mensich nez 10 ze vsech prirozenych cisel:") new_sequence_2 = take_while(podminka, sequence) print_sequence(new_sequence_2) print("Vyber prvku delitelnych tremi a mensich nez 20 ze vsech prirozenych cisel:") # nekonecna lazy sekvence sequence = range() # lze ji filtrovat filtered = filter(lambda x: x % 3 == 0, sequence) # a vybrat n prvku z nekonecneho poctu new_sequence = take_while(lambda x: x < 20, filtered) print_sequence(new_sequence) print() print("Kombinace remove() a take_while():") # nekonecna lazy sekvence sequence = range() # lze ji filtrovat removed = remove(lambda x: x % 3 != 0, sequence) # a vybrat n prvku z nekonecneho poctu new_sequence = take_while(lambda x: x < 30, removed) print_sequence(new_sequence) print() print("Predikat vracejici vzdy False:") # nekonecna lazy sekvence sequence = range() # predikat ktery vzdy vraci False new_sequence = take_while(lambda _: False, sequence) print_sequence(new_sequence)
Výsledky vygenerované předchozím příkladem:
Vyber prvku mensich nez 10 ze vsech prirozenych cisel: 0 1 2 3 4 5 6 7 8 9 ---------------------------------------- Vyber prvku mensich nez 10 ze vsech prirozenych cisel: 0 1 2 3 4 5 6 7 8 9 ---------------------------------------- Vyber prvku delitelnych tremi a mensich nez 20 ze vsech prirozenych cisel: 0 3 6 9 12 15 18 ---------------------------------------- Kombinace remove() a take_while(): 0 3 6 9 12 15 18 21 24 27 ---------------------------------------- Predikat vracejici vzdy False: ----------------------------------------
11. Přístup k prvkům nekonečné sekvence
Při práci s lazy sekvencemi, které obsahují nekonečný počet prvků, si musíme dát pozor na to, aby se náhodou nespustilo vyhodnocení celé (nekonečné) sekvence. V reálných programech k tomuto problému v naprosté většině případů nedochází, už jen z toho důvodu, že například funkce map (tu jsme si nepopsali, ale je taktéž součástí knihovny) jako svůj parametr akceptuje lazy sekvenci a jejím výsledkem je taktéž lazy sekvence.
My ovšem v následujících příkladech budeme muset zjistit a vypsat hodnotu alespoň několika prvků nekonečných lazy sekvencí. K tomuto účelu nám velmi dobře poslouží funkce nth, take a někdy taktéž poněkud složitější funkce take-while zmíněna v textu výše. Nejjednodušší z této trojice funkcí je nepochybně funkce nth, jež – jak jste již pravděpodobně z jejího názvu uhodli – vrací n-tý prvek sekvence, což většinou znamená, že se vyhodnotí i předchozích n-1 prvků (ovšem ve skutečnosti se výsledky ukládají do vyrovnávací paměti, takže někdy k vyhodnocení nedochází, alespoň ne v Clojure).
Příklad napsaný v Clojure:
user=> (def s (range)) #'user/s user=> (println (nth s 10)) 10 user=> (println (nth s 10)) 10 user=> (println (nth s 10)) 10
Zatímco funkce nth vrátí konkrétní prvek z lazy sekvence, je další užitečná funkce nazvaná take nepatrně obecnější, neboť ta vrací prvních n prvků lazy sekvence. Ovšem výsledkem není vektor či seznam těchto prvků, ale taktéž lazy sekvence, což znamená, že k vyhodnocení (získání) prvků dochází později a někdy taktéž vůbec ne. My ovšem v našich příkladech budeme výsledek funkce take vypisovat pomocí REPL, takže k vyhodnocení dojde vždy:
(user=> (println (take 10 s)) (0 1 2 3 4 5 6 7 8 9) user=> (println (take 5 (drop 10 s))) (10 11 12 13 14) user=> (->> (range) (drop 10) (take 5) println) (10 11 12 13 14)
12. Funkce nth a take v Pythonu
Nyní se pokusíme použít funkce nth a take v Pythonu. Obě zmíněné funkce jsou v knihovně clj implementovány, takže by s jejich použitím neměl být – alespoň zdánlivě – žádný problém. Začátek skriptu je stejný, jako v předchozích příkladech:
from clj.seqs import nth, take, range def hr(): print(40*"-") print() def print_sequence(sequence): for item in sequence: print(item) hr()
Vytvoříme nekonečnou sekvenci a třikrát po sobě se pokusíme přečíst její desátý prvek:
sequence = range() item = nth(sequence, 10) print(item) # pozor na rozdilne chovani! item = nth(sequence, 10) print(item) # pozor na rozdilne chovani! item = nth(sequence, 10) print(item)
Výsledek může být překvapující – pokaždé totiž dostaneme jiný prvek!:
10 21 32
Je tomu tak z toho důvodu, že v Pythonu nemáme skutečné lazy sekvence, ale „pouze“ generátory, které si pamatují svůj stav a funkce nth tento stav nenávratně změní.
Podobně je tomu při použití funkce take, která v Clojure vrátí prvních deset prvků (nekonečné) (lazy) sekvence:
sequence = range() s1 = take(10, sequence) s2 = take(10, sequence) s3 = take(10, sequence)
Výsledek bude opět odlišný od očekávaného stavu:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [10, 11, 12, 13, 14, 15, 16, 17, 18, 19] [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
13. Použití funkce flatten
V komunitě vývojářů používajících programovací jazyk Python se poměrně často diskutuje o „správné“ implementaci funkce typu flatten, tj. funkce, která dostane na vstupu rekurzivní datovou strukturu (například seznam seznamů) a vytvoří z ní jednorozměrnou sekvenci. V knihovně clj samozřejmě tato funkce existuje a její chování do značné míry odpovídá stejnojmenné funkci implementované v programovacím jazyce Clojure. Podívejme se tedy nejdříve na to, jak se tato užitečná funkce používá v Clojure:
(def colors ["red", "blue", "green", "yellow", "cyan", "magenta", "black", "white"]) (def c1 [colors [1 [2 [3 [4 [5 6 7 8 9]]]]]]) (def c2 [[c1] c1]) (println (flatten colors)) (println (flatten c1)) (println (flatten c2))
Výsledek by měl vypadat následovně:
(red blue green yellow cyan magenta black white) (red blue green yellow cyan magenta black white 1 2 3 4 5 6 7 8 9) (red blue green yellow cyan magenta black white 1 2 3 4 5 6 7 8 9 red blue green yellow cyan magenta black white 1 2 3 4 5 6 7 8 9)
V Pythonu můžeme postupovat naprosto stejným způsobem, pouze nesmíme zapomenout naimportovat správnou funkci:
from clj.seqs import flatten colors = ["red", "blue", "green", "yellow", "cyan", "magenta", "black", "white"] c1 = [colors, [1, [2, [3, [4, [5, 6, 7, 8, 9]]]]]] c2 = [[c1], c1] print(list(colors)) print(list(flatten(colors))) print() print(list(c1)) print(list(flatten(c1))) print() print(list(c2)) print(list(flatten(c2)))
Výsledky budou vypadat podobně, i když samozřejmě reprezentace seznamů a sekvencí bude při tisku odlišná od programovacího jazyka Clojure:
['red', 'blue', 'green', 'yellow', 'cyan', 'magenta', 'black', 'white'] ['red', 'blue', 'green', 'yellow', 'cyan', 'magenta', 'black', 'white'] [['red', 'blue', 'green', 'yellow', 'cyan', 'magenta', 'black', 'white'], [1, [2, [3, [4, [5, 6, 7, 8, 9]]]]]] ['red', 'blue', 'green', 'yellow', 'cyan', 'magenta', 'black', 'white', 1, 2, 3, 4, 5, 6, 7, 8, 9] [[[['red', 'blue', 'green', 'yellow', 'cyan', 'magenta', 'black', 'white'], [1, [2, [3, [4, [5, 6, 7, 8, 9]]]]]]], [['red', 'blue', 'green', 'yellow', 'cyan', 'magenta', 'black', 'white'], [1, [2, [3, [4, [5, 6, 7, 8, 9]]]]]]] ['red', 'blue', 'green', 'yellow', 'cyan', 'magenta', 'black', 'white', 1, 2, 3, 4, 5, 6, 7, 8, 9, 'red', 'blue', 'green', 'yellow', 'cyan', 'magenta', 'black', 'white', 1, 2, 3, 4, 5, 6, 7, 8, 9]
14. Funkce shuffle
V některých situacích (v mém případě například při psaní testů) je zapotřebí vzít vstupní sekvenci a náhodně v ní proházet jednotlivé prvky. Tuto operaci zajišťuje funkce nazvaná shuffle. Opět se nejdříve podívejme na to, jak se tato funkce používá v programovacím jazyce Clojure. Není to nic těžkého – pouze desetkrát vypíšeme sekvenci s náhodně proházenými prvky:
(def colors ["red", "blue", "green", "yellow", "cyan", "magenta", "black", "white"]) (dotimes [n 10] (println (shuffle colors)))
Výsledek bude vypadat například takto (pokaždé ovšem bude jiný):
[green cyan black white red magenta yellow blue] [cyan white black green yellow red blue magenta] [red green white black yellow blue cyan magenta] [black magenta red yellow blue green white cyan] [yellow blue white cyan magenta green red black] [cyan black yellow white green magenta red blue] [blue black magenta white yellow green red cyan] [yellow blue cyan white magenta red black green] [red cyan yellow black white blue green magenta] [green cyan black blue magenta red yellow white]
Použití stejnojmenné funkce v Pythonu:
from clj.seqs import shuffle colors = ["red", "blue", "green", "yellow", "magenta", "cyan", "white", "black"] for i in range(1, 10): print(list(shuffle(colors)))
Výsledky budou vypadat následovně:
['yellow', 'green', 'cyan', 'magenta', 'blue', 'black', 'white', 'red'] ['magenta', 'green', 'yellow', 'white', 'red', 'cyan', 'blue', 'black'] ['blue', 'red', 'black', 'cyan', 'yellow', 'white', 'magenta', 'green'] ['green', 'cyan', 'red', 'black', 'white', 'yellow', 'blue', 'magenta'] ['black', 'yellow', 'white', 'cyan', 'green', 'blue', 'red', 'magenta'] ['blue', 'cyan', 'white', 'black', 'magenta', 'red', 'green', 'yellow'] ['magenta', 'white', 'blue', 'green', 'yellow', 'cyan', 'black', 'red'] ['cyan', 'red', 'black', 'magenta', 'white', 'green', 'yellow', 'blue'] ['green', 'yellow', 'blue', 'black', 'red', 'cyan', 'magenta', 'white']
15. Rozdělení sekvence do podskupin s využitím funkce group-by
Sekvenci je možné rozdělit do skupin s využitím funkce vyššího řádu pojmenované group-by. Této funkci se kromě rozdělované sekvence předává i další funkce, jejíž návratová hodnota je použita pro rozpoznání skupiny (může se jednat o libovolnou hodnotu kromě nil). Podívejme se například na způsob rozdělení sekvence přirozených čísel na tři skupiny:
- Čísla dělitelná třemi
- Čísla, u kterých po vydělení třemi dostaneme zbytek 1
- Čísla, u kterých po vydělení třemi dostaneme zbytek 2
(def s (range 1 100)) (def groups (group-by #(mod % 3) s)) (def k (sort (keys groups))) (doseq [key k] (println key (get groups key)))
Výsledky:
0 [3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84 87 90 93 96 99] 1 [1 4 7 10 13 16 19 22 25 28 31 34 37 40 43 46 49 52 55 58 61 64 67 70 73 76 79 82 85 88 91 94 97] 2 [2 5 8 11 14 17 20 23 26 29 32 35 38 41 44 47 50 53 56 59 62 65 68 71 74 77 80 83 86 89 92 95 98]
Podobně můžeme rozdělit názvy barev podle délky názvu:
(def colors ["red", "blue", "green", "yellow", "cyan", "magenta", "black", "white"]) (def groups (group-by #(count %) colors)) (println groups)
Výsledky:
{3 [red], 4 [blue cyan], 5 [green black white], 6 [yellow], 7 [magenta]}
16. Funkce group_by v Pythonu
Naprosto stejným způsobem budeme postupovat v Pythonu, ovšem jméno funkce se pochopitelně muselo změnit z group-by na group_by:
from clj.seqs import group_by sequence = range(1, 100) print(group_by(lambda x: x % 3, sequence)) groups = group_by(lambda x: x % 3, sequence) keys = sorted(groups.keys()) for key in keys: print(key, groups[key])
Výsledek rozdělení čísel do tří skupin:
0 [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99] 1 [1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49, 52, 55, 58, 61, 64, 67, 70, 73, 76, 79, 82, 85, 88, 91, 94, 97] 2 [2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35, 38, 41, 44, 47, 50, 53, 56, 59, 62, 65, 68, 71, 74, 77, 80, 83, 86, 89, 92, 95, 98]
Rozdělení jmen barev podle délky jmen:
colors = ["red", "blue", "green", "yellow", "cyan", "magenta", "black", "white"] groups = group_by(lambda color: len(color), colors) print(groups)
Výsledkem je slovník s pěticí dvojic klíč-hodnota:
{3: ['red'], 4: ['blue', 'cyan'], 5: ['green', 'black', 'white'], 6: ['yellow'], 7: ['magenta']}
17. Sloučení několika sekvencí funkcí interleave
Dalo by se říci, že opakem funkce group-by je funkce pojmenovaná interleave. Ta slouží pro proložení prvků ze dvou či více sekvencí do sekvence nové. Nejprve se ze všech vstupních sekvencí získá stejný počet prvků určený nejkratší sekvencí. Následně se postupně vytvoří sekvence nová pravidelným prokládáním prvků všech (obecně zkrácených) vstupních sekvencí. Podívejme se na jednoduchý příklad, v němž nejprve vytvoříme sekvenci celých čísel a barev a posléze sekvenci celých čísel, barev a znaků hvězdičky:
(def s (range 1 10)) (def colors ["red", "blue", "green", "yellow", "cyan", "magenta", "black", "white"]) (println (interleave s colors)) (println) (println (interleave s colors (repeat 10 "*")))
Výsledky:
(1 red 2 blue 3 green 4 yellow 5 cyan 6 magenta 7 black 8 white) (1 red * 2 blue * 3 green * 4 yellow * 5 cyan * 6 magenta * 7 black * 8 white *)
18. Funkce interleave v Pythonu
Použití funkce interleave v Pythonu je prakticky totožné s výše uvedeným příkladem určeným pro Clojure. Takže jen krátce:
from clj.seqs import interleave, repeat def hr(): print(40*"-") print() def print_sequence(sequence): for item in sequence: print(item) hr() sequence1 = range(1, 10) sequence2 = ["red", "blue", "green", "yellow", "orange", "cyan", "white", "black"] print("Two sequences interleaved:") print_sequence(interleave(sequence1, sequence2)) print("Three sequences interleaved:") print_sequence(interleave(sequence1, sequence2, repeat("*", 10)))
Výsledky (nyní jsou prvky sekvencí vypsané pod sebou):
Two sequences interleaved: 1 red 2 blue 3 green 4 yellow 5 orange 6 cyan 7 white 8 black ---------------------------------------- Three sequences interleaved: 1 red * 2 blue * 3 green * 4 yellow * 5 orange * 6 cyan * 7 white * 8 black * ----------------------------------------
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes zmíněných demonstračních příkladů byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/lisps-for-python-vm. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, stále doslova několik kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
Alternativní (a přibližně ekvivalentní) příklady naprogramované přímo v Clojure:
# | Příklad | Adresa |
---|---|---|
1 | basic_usage.clj | https://github.com/tisnik/lisps-for-python-vm/blob/master/clj-library/basic_usage.clj |
2 | filters.clj | https://github.com/tisnik/lisps-for-python-vm/blob/master/clj-library/filters.clj |
3 | group_by.clj | https://github.com/tisnik/lisps-for-python-vm/blob/master/clj-library/group_by.clj |
4 | interleave.clj | https://github.com/tisnik/lisps-for-python-vm/blob/master/clj-library/interleave.clj |
5 | lazy_sequence_nth_take.clj | https://github.com/tisnik/lisps-for-python-vm/blob/master/clj-library/lazy_sequence_nth_take.clj |
6 | lazy_sequence_take_while.clj | https://github.com/tisnik/lisps-for-python-vm/blob/master/clj-library/lazy_sequence_take_while.clj |
7 | flatten.clj | https://github.com/tisnik/lisps-for-python-vm/blob/master/clj-library/flatten.clj |
7 | shuffle.clj | https://github.com/tisnik/lisps-for-python-vm/blob/master/clj-library/shuffle.clj |
20. Odkazy na Internetu
- clj – repositář s knihovnou
https://github.com/bfontaine/clj - clj 0.1.0 – stránka na PyPi
https://pypi.python.org/pypi/clj/0.1.0 - Clojure aneb jazyk umožňující tvorbu bezpečných vícevláknových aplikací pro JVM (4.část – kolekce, sekvence a lazy sekvence)
https://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-4-cast-kolekce-sekvence-a-lazy-sekvence/ - Clojure a bezpečné aplikace pro JVM: sekvence, lazy sekvence a paralelní programy
https://www.root.cz/clanky/clojure-a-bezpecne-aplikace-pro-jvm-sekvence-lazy-sekvence-a-paralelni-programy/ - Python becomes a platform
https://khinsen.wordpress.com/2012/03/15/python-becomes-a-platform/ - Python becomes a platform. Thoughts on the release of clojure-py
https://news.ycombinator.com/item?id=3708974 - SchemePy
https://pypi.org/project/SchemePy/ - lispy
https://pypi.org/project/lispy/ - Lython
https://pypi.org/project/Lython/ - Lizpop
https://pypi.org/project/lizpop/ - Budoucnost programovacích jazyků
http://www.knesl.com/budoucnost-programovacich-jazyku - LISP Prolog and Evolution
http://blog.samibadawi.com/2013/05/lisp-prolog-and-evolution.html - List of Lisp-family programming languages
https://en.wikipedia.org/wiki/List_of_Lisp-family_programming_languages - clojure_py na indexu PyPi
https://pypi.python.org/pypi/clojure_py - PyClojure
https://github.com/eigenhombre/PyClojure - Hy na GitHubu
https://github.com/hylang/hy - Hy: The survival guide
https://notes.pault.ag/hy-survival-guide/ - Hy běžící na monitoru terminálu společnosti Symbolics
http://try-hy.appspot.com/ - Welcome to Hy’s documentation!
http://docs.hylang.org/en/stable/ - Hy na PyPi
https://pypi.org/project/hy/#description - Getting Hy on Python
https://lwn.net/Articles/596626/ - Programming Can Be Fun with Hy
https://opensourceforu.com/2014/02/programming-can-fun-hy/ - Přednáška o projektu Hy (pětiminutový lighttalk)
http://blog.pault.ag/day/2013/04/02 - Hy (Wikipedia)
https://en.wikipedia.org/wiki/Hy - Clojure home page
http://clojure.org/ - Clojure Sequences
http://clojure.org/sequences - Making Clojure Lazier
https://clojure.org/reference/lazy - Clojure Data Structures
http://clojure.org/data_structures - Clojure
https://en.wikipedia.org/wiki/Clojure - Clojars
https://clojars.org/ - Seznam knihoven na Clojars
https://clojars.org/projects - Clojure – Functional Programming for the JVM
http://java.ociweb.com/mark/clojure/article.html - Clojure quick reference
http://faustus.webatu.com/clj-quick-ref.html - 4Clojure
http://www.4clojure.com/ - ClojureDoc (rozcestník s dokumentací jazyka Clojure)
http://clojuredocs.org/ - SICP (The Structure and Interpretation of Computer Programs)
http://mitpress.mit.edu/sicp/ - Pure function
http://en.wikipedia.org/wiki/Pure_function - Funkcionální programování
http://cs.wikipedia.org/wiki/Funkcionální_programování - Čistě funkcionální (datové struktury, jazyky, programování)
http://cs.wikipedia.org/wiki/Čistě_funkcionální - Dynamic Languages Strike Back
http://steve-yegge.blogspot.cz/2008/05/dynamic-languages-strike-back.html - Scripting: Higher Level Programming for the 21st Century
http://www.tcl.tk/doc/scripting.html - Threading macro (dokumentace k jazyku Clojure)
https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/-> - Understanding the Clojure → macro
http://blog.fogus.me/2009/09/04/understanding-the-clojure-macro/ - Emacs LISP
https://www.root.cz/clanky/historie-vyvoje-textovych-editoru-eine-zwei-emacs/#k08 - Programovací jazyk LISP a LISP machines
https://www.root.cz/clanky/programovaci-jazyk-lisp-a-lisp-machines/ - Speciální formy, lambda výrazy a makra v programovacím jazyku LISP
https://www.root.cz/clanky/specialni-formy-lambda-vyrazy-a-makra-v-programovacim-jazyku-lisp/ - Programovací jazyky používané (nejen) v SSSR (část 3 – LISP)
https://www.root.cz/clanky/programovaci-jazyky-pouzivane-nejen-v-nbsp-sssr-cast-3-ndash-lisp/