Komentáře jsou nejdůležitější částí kódu

Už je tomu šest let, co jsem psal o hezkém kódu na Zdrojáku. Sice bych to dnes po zkušenostech na několika velkých projektech napsal lehce jinak, ale stejně je skvělé, jak jsem se sám tenkrát k něčemu takovému dostal a dokázal sdílet s ostatními. Každopádně – popisoval jsem, že komentáře je lepší nepsat. Pokud je nutkání napsat komentář, mnohem lepší varianta je upravit kód, aby byl samopopisný.

S tím stále souhlasím. Dostal jsem se však do bodu, kdy i když je kód popisný a hezký, komentář napíšu. To hned z několika důvodů. Ukážu na příkladech. První příklad budiž snippet z Flasku:

@app.before_request
def require_login():
    if is_user_logged_in():
        return

    if flask.request.endpoint == 'static':
        return

    view = get_current_view()
    if not view:
        return

    if is_view_public():
        return

    flask.abort(403)

Když přečtu za sebou, jak je, hned je zřejmé, co se děje. Volně přeloženo do češtiny: ověř přihlášení před zpracováním jakéhokoliv požadavku. Ale proč se to netýká statických souborů? Statické soubory nejsou chráněné? A vlastně – proč statické soubory řeší Flask a ne něco rychlejšího? Komentáře hned objasní…

@app.before_request
def require_login():
    if is_user_logged_in():
        return

    # Static files are not protected. Only in development without nginx.
    if flask.request.endpoint == 'static':
        return

    # Not found page is not protected.
    view = get_current_view()
    if not view:
        return

    if is_view_public(view):
        return

    flask.abort(403)

Pojďme na další ukázku z jednoho malého scriptu, který jsem nedávno psal:

def load_config_variable(key):
    variable = connection.configserver.variable.getAttributes(key)
    utc = dateutil.tz.tzoffset(None, 0)
    local = dateutil.tz.tzlocal()
    variable['since'] = variable['since'].replace(tzinfo=utc).astimezone(local)
    return variable

Opět je jasné, co se děje. Načítám proměnnou z konfiguračního serveru. Jenže, proč se tu přepočítává datum do jiné časové zóny? A proč zrovna do lokální a ne do jiné? Co se stane, když se změní čas z letního na zimní a obráceně?

#FIXME: Konexe ma spanou implementaci casovych zon. Z aware RPC datetimu prevadi
#       na naive Pythoni datetime (se ztratou informace o zone se zapocitanim
#       offsetu). Takze tu mame naive datum v UTC. Musime ho obohatit tedy o
#       zonu, aby slo do configserveru zpet zapsat.
#       Smazat jakmile se opravi prace se zonou v RPC.

Nyní už není pochyb, proč to tu je. Dokonce i co je potřeba udělat, aby bylo zase v pořádku.

Další ukázka z Reactu:

let EntryActions = {
    (...)
    reportProblem: function(id) {
        AppDispatcher.handleAction({
            actionType: RSSConstants.ENTRY_REPORT_PROBLEM,
            id: id,
        });
        EntryService.reportProblem(id);
    }
    (...)
};

Z úryvku je patrné, že se při odpálení akce z UI vyšle, že je s nějakým záznamem problém. To se pomocí service odešle na server. Ale co když se komunikace se serverem nezdaří? Na to tu nikde není žádný kód, to vypadá na chybu. S komentářem opět dodá informaci o úmyslu:

// Dve moznosti:
//  1) Jen na par tydnu, nez se vyladi zaznamy. Jelikoz se brzy smaze,
//     nema tedy smysl resit rollback v UI.
//  2) Nebo se bude ladit donekonecna, pak ale bude potreba i vyzadovat komentar,
//     aby se dobre zpracovavalo, a tudiz udelat jinak, tedy nema smysl resit rollback v UI.

Další příklad mohou být Jasmínové testy. Kolega napsal vše v pořádku, ale u jednoho describe měl pouze jeden it s prázdným popisem. Zdálo se mi to jako chyba. Přitom to měl promyšlené – struktura testů byla dle logických celků, ale jedna malá drobnost do toho ne úplně zapadala. Ale zachoval to, což bych při návštěvě kódu pravděpodobně změnil. Poprosil jsem ho tedy o komentář, jak zamýšlel strukturu testů, aby další kolemjdoucí dodržel.

Jinými slovy se snažím říct, že vy možná právě teď komentáře u čitelného kódu nepotřebujete. Komentáře s úmyslem jsou ale pro ostatní, včetně vás za pár měsíců či týdnů. Záleží jak dobře paměť slouží. Protože s takovými komentáři se lze vyhnout zbytečným chybám. Líbil se mi na toto téma tweet s následujícím gifem a popiskem „Why is this code here? We don't need this.“. :-)

Dokonce jsem zjistil, že mi pomáhá psát komentáře průběžně při vývoji všeho možného. Často sahám na spoustu míst a měním několik věcí najednou. Prototypuji. Rychle kód prasím. Až pak ho uhlazuji. Přijde mi to rychlejší než sáhodlouze přemýšlet, jak to udělat hezky na první dobrou. Proto si píšu komentáře, co se mi honí hlavou, a při uhlazování je postupně zpracovávám. Ve výsledku commitnu minimum napsaných komentářů, neb je proměnním v samopopisný kód a zůstanou jen ty komentáře vysvětlující úmysl.

A jelikož nejlepší komentář vysvětluje úmysl, přijde mi, že je častokrát důležitější, než kód samotný. Udělal jsem si takový malý průzkum a zjistil, že neexistuje žádný skin, kde by komentář nebyl téměř neviditelný. Proto jsem si sám ve všech používaných editorech nastavil komentáře tučně a křiklavě červené. Mám to tak přes měsíc a naprostá spokojenost.

Už jen škoda míst, kde nelze psát komentáře. Například v debianích souborech bych rád napsal, proč se například do deb balíku dávají i nějaké soubory a jiné naopak ne. Když to pak nejde, musí se napsat do dokumentace bokem. Jenže já mám tak rád, když se dokumentace generuje z kódu a nemusím na to myslet…

Na závěr taková perlička. Před dvěma lety jsem napsal takový ne zrovna hezký (asi tak jako samotný úkol), ale funkční, algoritmus na automatické generování práv v aplikaci. Napsal jsem k pár řádkům následující komentář (na který jsem úspěšně zapomněl) a kluci před pár dny našli:

# So you found a bug here. It's probably 2016. You know, it can't be
# fixed. You have to rewrite it completely. And what about your trip to
# Australia, was it good?

:-)

Scrum není ztráta času

Občas mají vývojáři názor, že scrum stěžuje práci. Protože přináší s sebou spoustu schůzek a nemožnost změnit sprint. Místo aby umožnil vývojáři sedět u klávesnice a programovat. Že to je vlastně jen pro managery. To si osobně nemyslím a mám naopak zkušenost, že scrum chrání především nás, vývojáře. Rozhodl jsem se tedy sepsat, co mi scrum přináší.

Volnost.

Asi tímhle slovem bych celý přínos shrnul. Díky scrumu mohu přestat myslet na spoustu věcí a mám volnost v tom, do čeho se kdy pustím. Před scrumem totiž vývoj probíhal (u nás) tak, že byl dlouhý seznam všeho možného. Dlouhodobých i krátkodobých cílů, velkých i malých úkolů. Ve větším počtu lidí (řekněme dva a více) už začíná ztráta přehlednosti a je tedy těžké říct, kdy co může být hotové. To pak vedlo produkťáky k tomu, že když se změnila priorita (například když něco trvalo moc dlouho, raději se chtěli pustit do něčeho jiného; nebo jednoduše změnili názor), okamžitě nám diktovali, co máme dělat. Někdy i kdo.

Se scrumem naopak všichni víme (resp. vždy se můžeme podívat), co se právě dělá a co nás čeká v brzké době i jaké jsou dlouhodobější cíle. Produkťáci si udělají priority a na ty nejbližší se zaručí, že tak zůstanou. Na oplátku vývoj nejbližší úkoly ohodnotí a řekne, které určitě stihne během následujících dvou týdnů (prostě za sprint). Tím my, vývoj, víme, jaké priority jsou, a produkt ví, s čím může počítat. Radost na obou stranách bez diktatury, kdo co kdy má dělat.

Na tabuli pak visí spoustu úkolů a každý z týmu má možnost si práci vybrat a zorganizovat, jak mu vyhovuje. Žádný větší projekt bych už nechtěl dělat jinak. I na svém projektu, kde si sám sobě dělám i produkťáka, si řadím úkoly dle priorit a pak jako vývojář si beru dle aktuální chutě ty nejbližší.

Jako vedlejší účinek pak všichni mají možnost i vědět, co se děje na celém projektu. Což mi v případě vedení týmu zjednodušilo práci a v roli programátora ukonejší zvědavost a touhu pomoct v případě problému.

Samozřejmě nic není ideální a tak dalšími „zbytečným“ schůzkám jako retro můžeme proces dál zlepšovat. Scrum totiž není o tom dodržovat nějaké desatero. Je to o tom si to nastavit tak, aby co nejvíce vyhovovalo. A nejen zlepšovat proces vývoje, ale i samotný vývoj a komunikaci v týmu. Programátoři často žijí jen programováním, ale je potřeba budovat vztahy i kolem. S produkťáky, administrátory, obchodníky, … Lepší komunikace v týmu i mimo něj podstatně zpříjemní práci.

Mimochodem scrummaster je člověk, který se snaží sám sebe vyřadit z práce. Jeho cíl je, aby nebyl potřeba, aby tým fungoval jako hodinky i bez něj. Měl by nastavit vývoj tak, aby vše fungovalo, a pak jen dohlížet, zda tomu tak stále je. U nás platí, že jakmile scrummaster něco řeší, to něco nějakým způsobem drhne. Tedy na jedné straně sice scrum přidává meetingy, na druhé straně ubírá, neb přidává kvalifikovaného člověka, který řeší různé problémy kolem.

A to mi dává scrum. Chrání mne před neideální realitou požadavků. Samozřejmě není dokonalý a nic nebude, ale je to mnohem lepší, než bez něj. Pro mne určitě.

Co mi dalo pět let na jednom projektu

Podobnost s předchozím článkem není ponechána náhodě. Je to takové volné pokračování myšlenek. Na interní službě SOS (Seznam Obchodní Systém) v rámci Seznamu jsem byl přesně pět let a jeden měsíc. Měsíc leden jsem tam zůstal navíc ještě jako podpora, kdy jsem hodně pároval a předával vědomosti nejen o projektu. To mi dalo i zajímavou příležitost vidět před odchodem celý projekt z trochu jiné perspektivy a čas na zamyšlení.

Díky tomu jsem si všiml jedné velmi zajímavé věci, které si lze všimnout právě po delší době na „jednom“ místě. Lidi se totiž točí přibližně co dva/tři roky a tak jsem poznal různé programátory a viděl jejich pohled na stejný kód a práci jejich předchůdců. A jelikož jsem znal i některé předchůdce, udělal jsem si obrázek.

Obrázek mi vyšel takový, že programátoři mají tendenci neustále něco zlepšovat. Aspoň to je to, co si myslí. Ve skutečnosti chtějí spíše měnit k obrazu svému. Jak si myslí, že je to správně. Realitou ale je, že nikdo neví, jak je to správně. I když víme, co vše systém má umět, nebudeme schopni si jít sednout a na zelené louce postavit krásnou vilku. Kód, který tam nyní je, by nebylo možné použít a nejlepší by bylo sáhnout po něčem novém. Novém Pyhtonu, jiné komunikační vrstvě, využít jiné patterny, napsat web jako SPA nebo naopak nepsat už jako SPA atd.

Tedy dnes sice víme, co jsme neměli dělat tak, jak jsme udělali. Jenže s dnešními technologiemi zase nevíme, čemu se vyhnout tentokrát. Protože to je prostě něco nového a musí se zase prozkoumat. Jinými slovy opět udělat chyby. Což je důležité si uvědomit, než se začne křičet jak někdo mohl něco napsat jak napsal.

Neříkám, že by se mělo smířit, jak kód vypadá, a přidat další bez žádného vylepšení. Právě přesný opak: zlepšovat! Ale ne neustále to samé. Protože mi přijde, že se často řeší ty samé věci dokola. A jakmile se dostane do jednoho bodu, začne se mluvit o tom, že to vlastně není dobře, a předělává se znovu. Třeba i do více méně verze, jak bylo dřív.

Byl jsem v tomhle ohledu mírný a dnes už vím, že bych měl přitvrdit. Například struktura kódu, validace vstupních dat, úroveň zobecňování, … jsou věci, které se řeší téměř všude a neustále, každý na to má svůj názor a jiné zkušenosti a nikdy řešení nebude perfektní. Protože perfektní neexistuje. Takže nemá smysl se snažit o dokonalost a neustále měnit ty samé věci.

Málokdo (čti opravdu málokdo, nejsem to ani já a velice pravděpodobně ani vy) dokáže napsat nadčasový kód. Podobně jako málokterý spisovatel dokáže napsat nadčasovou knihu či scénárista film.

Je potřeba se smířit s tím, že před námi napsal kód „prostě jen člověk“ a nezabývat se zbytečným přepisem. Na každém projektu je spoustu zajímavých a mnohem důležitějších oblastí, které je potřeba řešit. Stačí se rozhlédnout, udělat si seznam a priority. A řešit to, nikoliv jestli v pár let starém kódu je [doplň si jakoukoliv nekritickou věc, která tě irituje].

Nová láska Haskell

Už tomu jsou dva roky, co jsem si řekl, že chci poznat svět funkcionálního programování. O Haskellu jsem slyšel, že je to ten správný výběr, ale akademický. Takže jsem sáhl po jednom z těch populárnější, Scale. Zkusil jsem si kurz na Courseře a… no, nedokončil jsem. Scala mi nějak k srdci nepřirostla. Nebavilo mne v ní programovat. Asi tam na mne bylo stále moc Javy a málo prostoru pro funkcionální styl. Takže jsem přeci jen nakonec skončil u Haskellu.

Začít se učit funkcionální styly v Haskellu pro jeho puričnost je náročné. Několikrát mi málem explodovala hlava, jak je vše úplně jinak, než jsem doposud znal. Ale přežil jsem a jak se říká, co tě nezabije… to si zamiluješ. :-)

Uvědomil jsem si to ve chvíli, kdy jsem napsal Pytohní kód, který na mne zařval výjimkou. Že prý druhý řádek není callable…

ZeroDict = defaultdict(lambda: 0)
d = ZeroDict({'a': 1, 'b': 2})

Aneb technika známá jako partial application ve funkcionálním světě. Každá funkce bere ve skutečnosti pouze jeden parametr, což se v Haskellu velmi často prakticky využívá.

Jakmile jsem chybu opravil, zařvalo na mne NoneType object has no … Něco, co se mi v Haskellu nemůže stát. Prázdná hodnota totiž neexistuje. Pokud se může něco pokazit, musí se taková událost explicitně vyjádřit. Což může znít děsivě, ale díky monádám (žádný strach) se kód neprasí samými podmínkami a pracuje se s ním mnohem lépe.

Například jsem si udělal jednoduchý RSS a Atom parser. Samotné poskládání dílčích celků dohromady vypadá nějak takto:

getSourcesFromDatabase >>= fetchFeeds >>= mapM createQuery >>= commitQueries

A pokud se něco po cestě pokazí, script nespadne. Normálně doběhne a každá část se může rozhodnout, jak se situací naložit. Může to znít, že nakonec ty podmínky oddře spodnější vrstva. Ano, musí se kód přizpůsobit, každopádně sám jsem podmínku v Haskellu nepoužil. Žádnou! Aspoň ne v podobě, jakou ji běžně známe. Ani cyklus.

Další skvělá věc je totiž pattern matching. Dalo by se to lehce přirovnat k overloaded funkcím na steroidech. Příklad k parsování RSS či Atom feedu (pro lepší pochopení: vstup je seznam tagů jak jdou za sebou v XML struktuře, včetně komentářů či textu):

parseFeed :: Tags -> Maybe Feed
parseFeed [] = Nothing
parseFeed ((TagOpen "rss" _):tags) = parseRss tags
parseFeed ((TagOpen "feed" _):tags) = parseAtom tags
parseFeed (_:tags) = parseFeed tags -- Skip XML declaration and comments at the beggining.

Hledá se známý tag rss či feed a podle toho se provede další parsování. Jinak se pokračuje dál. A pokud nenajde, vrátí se Nothing, což je možná hodnota typu Maybe. Při použití se pak musí s takovou možností explicitně počítat. Ale jak bylo vidět výše, monády (což není nic jiného, než design pattern) to podstatně zjednodušují.

Navíc vše je krásně vidět, čímž mám na mysli typy. Haskell je velice chytrý a umí si typy téměř vždy odvodit sám. Ale mohu mu je předat a pomoct si tak při debugování. Jinými slovy Haskell je silně typový, ale zároveň nehází programátorovi klacky pod nohy.

Na Haskellu je hezká další věc, na kterou nemám vhodnou ukázku, takže si půjčím jak najít pravoúhlý trojúhelník s obvodem 24 jednotek z Learn You a Haskell for Great Good:

ghci> let allTriangles = [(a,b,c) | c <- [1..], b <- [1..c], a <- [1..b]]
ghci> let solutions = filter (\(a,b,c) -> a^2 + b^2 == c^2 && a+b+c == 24) allTriangles
ghci> take 1 solutions
[(6,8,10)]

A to, že se problémy neřeší zapsáním algoritmu, ale matematickým zápisem – udělá se množina možných řešení (klidně nekonečných) a aplikují se různé transformace, kterými se odfiltrují nevhodná. Až zůstanou jen ta správná řešení.

Hm, opravdu nekonečných? No jasně! Však se mrkněte na ukázku, konkrétně právě [1..] generuje čísla do nekonečna. Haskell je totiž, jako správný programátor, lazy. Dokud nemusí, nic neprovede. Pokud spustíte dlouho trvající (či nekonečnou) funkci, ale nepoužijete její výsledek, Haskell ji vůbec nevykoná. Což pak otevírá spoustu zajímavých možností řešit úkoly s nekonečnem. Jako třeba tu, kde chci jen první řešení, takže jakmile se najde, zbytek se vůbec nevyhodnotí.

Bohužel to taky má své stinné stránky. Sice to v některých případech oproti ostatním jazykům problémy řeší, v jiných naopak je potřeba dávat velký pozor. Aby něco takového fungovalo, musí si Haskell pamatovat stack, jak v případě potřeby danou hodnotu vypočítat. Což u velkých struktur může znamenat stack overflow. Tedy programátor na to musí myslet a na správných místech použít ty správné funkce či jinak vynutit vypočítat hodnotu, protože to je na nějakém místě prostě efektivnější.

Abych to shrnul: nikdy jsem si nemyslel, že se mi bude líbit jazyk, kde nejsou podmínky, cykly a prázdné hodnoty. Přišlo mi něco takového jako bláznovství. Ale po seznámení s Haskellem se mi takové praktiky opravdu líbí. Bohužel si myslím, že díky puričnosti, za což je často odsuzován, se do praxe moc nedostane. Ale osobně se mi to, překvapivě, opravdu líbí. Myslím si, že právě striktní oddělení pure funkcionálních funkcí dává méně prostoru pro chyby.

Mimochodem v Haskellu nejde debugovat s print příkazy jako třeba v Pythonu. Nejlepší způsob jsou testy. Takže mne to hezky nutí co nejvíce kódu umístit do lehce testovatelných pure funkcí a psát řádné testy vždy při vývoji, nikoliv až potom. A tím jsem zjistil, že TDD je pouze mentální switch. Nijak to nekomplikuje práci, je potřeba si na takový postup pouze zvyknout.

Pokud se chcete na Haskell také podívat, doporučuji tyto zdroje:

Roztříštěnost JavaScriptích knihoven

JavaScript mám docela rád. Kór dnes s ES6, ES7 a Reactem je psaní SPAček radost. Dokud není potřeba využít nějakou knihovnu. To pak přestává být sranda. Původně jsem chtěl napsat do nadpisu nepoužitelnost, ale nakonec lze knihovny nějak použít. Je s tím však spousta trápení.

Jeden z příkladů může být samotný React a známé a používané knihovny. Vyšel nový React 0.14, který přinesl zajímavé featury. Především stateless komponenty. Samozřejmě, že po pár týdnech jsem se rozhodl novinek využít. Jenže problém – používám taky react-bootstrap a podpora je až od verze 0.27. O pět čísel větší než s kterou jsem web napsal. Za tu dobu samozřejmě stihli sáhnout přesně na ty komponenty, které používám. Takže jsem šel upravovat čerstvě napsaný kód…

Což ještě není to nejhorší, horší je závislost na knihovnu, která na kompatibilitu kašle. Pak jen koukáte do konzole, jak React warninguje, a doufáte, že autor knihovny to také upraví. Nejpozději do vydání nové verze, která warningy transformuje na errory. S takovou zkušeností mi přijde, že se vyplatí záviset jen na populární knihovny a zbytek si udělat sám.

Má to ještě jeden ironický důvod. Možnost výběru je zlo. Pro různé úlohy existuje bambilion různých balíků. Každý si napsal něco a řekl si „to se bude někomu hodit, zveřejním to“. Taková blbost! Nyní je těžké se v nepřeberném množství zorientovat. S absencí dokumentací (pahýly mezi dokumentace nepočítám) je pak těžké najít, co vlastně řeší můj problém. A hlavně aby taková knihovna byla udržovaná a stabilní, tedy neprefixovaná nulou.

Další extrémní příklad je pořádná výzva: ES6 a pár ES7 featur, React, JSX. Zkuste s takovou kombinací rozchodit testy. A to ještě není nejhorší varianta! S takovými AMD moduly to je ještě větší sranda. V práci jsme narazili na bug v testovací knihovně. Možnosti jsou dvě – buď se o verzi vrátit. Nebo ve světě JS je naopak vždy dostupná novější verze, takže upgradovat. To nám ale zařvalo z důvodu absence nejnovějšího Nodu. Čerstvého Node 4. Na Debianu samozřejmě není dostupný, ale ne protože to je Debian, kde jsou balíky se zpožděním, ale protože to chce nový gcc dostupný až pod Jessie. Tedy: chceš testovat JavaScript? Upgradni si operační systém. Jo a taky si to zkompiluj…


Lehce to zveličuji. JavaScript se mi líbí. Občas ale dokáže vytočit. Proto chci spíš šířit následující zprávu, která se nemusí nutně vztahovat na JavaScriptí svět (jen to pro něj patří z důvodu spoustu edge novinek dvojnásob):

Chápu, každý chce být autorem populární knihovny. Populární knihovna ale nevznikne rychlým nabastlením, zveřejněním a doufat v open source boha. Už na začátku je potřeba se ke kódu chovat jako bych ho měl maintenovat do konce života. Což znamená si dobře promyslet API hned při prvním návrhu, aby se později měnilo minimálně. Také to znamená být připraven řešit všechny use-casy. Jak by se asi používalo request-get, request-post, request-json, … pokaždé od jiného autora? V neposlední řadě řádně zdokumentovat, co vlastně moje knihovna řeší a popsat jak s ukázkami.

Databáze v testech?

Včera jsem měl na první české PyCON konferenci přednášku o pytestu. Na Twitteru se rozjela diskuze o tom, že můj ukázkový test není unittest. Což je teda úplně mimo moji přednášku, protože jsem mluvil o unittestu jako toolu, nikoliv typu testu, vs. pytestu. A integrační test jsem vybral, protože je to prostě dobrá ukázka pro běžnou praxi.

Každopádně bych chtěl sdílet můj názor na toto téma. V Twitter vlákně se řeší několik věcí – udržovatelnost takových testů, zda mít různé typy separátně, spolehlivost, schopnost najít co nejvíce chyb a hlavně teorie. A jak já rád říkám, teoreticky mezi teorií a praxí není rozdíl, ale prakticky… :-)

Samozřejmě, nejlepší možný test je unittest, který nepotřebuje nic. Žádné cizí rozhraní, žádnou databázi, žádný přístup na disk ani nikam jinam, prostě nic. Pokud něco takového lze udělat a otestuje se tím vše potřebné, pak máte vyhráno. Pak vás nemusí zajímat boj s integračními testy. I to je důvod, proč Selenium je pro puristy tak děsivé.

Jenže v praxi to tak nefunguje. V praxi často něco čteme a zapisujeme. Můžeme takovou aplikaci otestovat tak, že vrstvu zařizující komunikaci s databází či něčím jiným nějak nahradíme/vyřadíme. Jenže, co jsme tím získali? Nic moc ve skutečnosti. Máme otestované algoritmy a provázanost naší aplikace, ale vůbec nevíme, zda funguje. Mohu otestovat, že moje aplikace správně vygenerovala XML a nikam ho neodeslat. Jenže po nasazení mi mohou klidně poslat neskutečné množství uhlí

OK, špatný příklad. Ale ten příběh mne fakt pobavil. :-) Vezměme jiný příklad: chci otestovat statistiky. Ano, mohu při testování podsunout aplikaci data, která chci, a podívat se, zda se čísla přelila takhle tam, tohle zase tam, a výsledek je takovýhle. Jenže… jenže po nasazení do provozu zjistím, že v SELECTu je překlep v jednom případě. Který sice testuji, ale bez databáze.

A takhle to je se spoustou aplikací. Proto, abychom měli větší jistotu, testujeme s databází. A pokud je to pouze s databází, říkáme tomu stále interně unittest. Pokud je to unittest bez databáze, říkáme tomu pure unittest. Pokud voláme i cizí rozhraní, zapisujeme na disk, …, říkáme tomu integrační test.

Řešili jsme, jak takové testy rozdělit. Původně jsme je měli rozdělené na „unittesty“ a na integrační testy. Ale byl v tom guláš, co kde vlastně testujeme a proč. Někdy bylo prostě vhodnější nechat integrační test, někde zase naopak. Takže testování nějaké funkcionality máme v jednom souboru a rozdíl mezi testy řešíme označením pomocí dekorátoru @pytest.mark.*. Tak hned vidím všechny testy pohromadě.

A jak nám to funguje? Velice dobře. Testy máme rychlé, v průměru 125 ms na test (s tím, že to hodně zvedá především pár integračních testů kvůli účetnictví pod Windowsem). S databází nemáme problémy. Dokonce CI tool (u nás Jenkins) si pro každý běh nejprve vytvoří novou databázi (cca deset vteřin). Tedy máme pod kontrolou, co v databázi je. Najde nám to více chyb. Nespočet chyb bylo v SQLku… Nemluvě o tom, že nemusíme složitě mockovat.

Abych to shrnul: integračních testů se nezbavíme a jsou potřeba. S knihovnou unittest je těžké takové testy udržovat a měli jsme s tím opravdu problémy. Ale díky pytestu a tipům, o kterých jsem mluvil a dříve i psal, už problémy žádné nemáme, ba dokonce je to stejně snadné jako jednotkové testy.

P.S.: Omlouvám se všem puristům za zemřelá koťátka. :-)

Zbytečné optimalizace

Vývojáři mají skvělý dar. Optimalizovat to, co je nejvíc zbytečné. Aneb příklad z praxe. :-)

Potřebovali jsme funkci batches s následujícím chováním podporující jakýkoliv iterable:

>>> list(batches.batches(range(10), 5))
[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]
>>> list(batches.batches(range(10), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

Udělali jsem tedy základní implementaci během minutky:

def batches(iterable, batch_size):
    bucket = []
    for item in iterable:
        bucket.append(item)
        if len(bucket) == batch_size:
            yield bucket
            bucket = []
    if bucket:
        yield bucket

Jenže… jenže to nevypadá sexy. Přeci to musí jít udělat nějak cool! A taky efektivněji. Například pro velký batch_size to musí být náročné. Pojďme zkusit udělat generátor generátorů.

def batches(iterable, batch_size):
    def batch_generator():
        for x in range(batch_size):
            yield next(iterable)

    while iterable:
        yield batch_generator()

Vypadá to docela hezky. Jenže to má zásadní problém. Předchozí ukázka totiž nikdy neskončí. Jakmile se dojede vstupní iterable, funkce bude generovat prázdné generátory do nekonečna. To je potřeba nějak vyřešit…

První nápad je odchytávat StopIteration. Co ale dál? Během chvilky jsme se dostali do velmi nehezkých konstrukcí a stejně zůstali v nekonečné smyčce. Kolegu napadlo si z vnitřního iterátoru předávat, zda bude něco následovat nebo ne. Idea je taková, že si první elementy ohandluju ručně nějak takto:

def batches(iterable, batch_size):
    def batch_generator():
        try:
            first = next(iterable)
        except StopIteration:
            yield False  # Hele, uz nic neprijde, tak to stopni.
        else:
            yield True  # Jasne, jeste neco prijde.
            yield first

        for x in range(1, batch_size):
            yield next(iterable)

    while True:
        gen = batch_generator()
        if next(gen):  # Prvni polozka je rucne vlozena.
            yield gen
        else:
            break

Super! Máme generátory. Generují dokonce správně. Jenže… jenže to vypadá opravdu ošklivě. Navíc je podporován jen takový vstup, který zvládne next. Zkusili jsme testy, zda jdeme dobrým směrem a… horší než jednoduchá jasná implementace! Možná to šetří pamětí, ale podstatně trpí výkon kvůli samému generování.

Tak znovu na začátek a jinak a lépe. itertools obsahují funkci groupby. S pomocí Google jsme došli k následujícímu řešni:

def batches(iterable, batch_size):
    iterable = iter(iterable)
    counter = itertools.count()

    def func(key):
        return next(counter) // batch_size

    for key, group in itertools.groupby(iterable, func):
        yield group

Vypadá elegantně, ale pokud k tomu nebude výklad třikrát tak dlouhý, jako celá funkce, nebude jasné, co se děje. A výsledek? Taky pomalejší. Výkon s groupby se od předchozího kódu moc neliší.

Závěr: neoptimalizujte. Optimalizujte až v případě, kdy je to opravdu potřeba. Raději pište čitelný kód. A pokud už bude potřeba optimalizace, ten čitelný kód nemažte – nechte ho vedle toho optimalizovaného. Aspoň budete moci rychle přečíst, co daná funkce dělá, a především zkontrolovat, zda ta zoptimalizovaná dělá skutečně to, co má.

Matka moudrosti: jak testovat

Nic z následujícího textu není nové. Určitě to někdo v podobném znění už někde sepsal. Ale pořád se na to naráží a opakování je matka moudrosti. :-)

Extrémní TDD říká, že by se měl napsat minimální test, počkat až selže, poté napsat minimální kód pro průchod testu, ověřit funkčnost, rozšířit test a tak pokračovat do nekonečna. Například:

  1. Napíšu test na zavolání funkce add.
  2. Test selže, protože funkce add neexistuje.
  3. Napíšu funkci add. Jen definici, aby ji šlo zavolat.
  4. Test projde.
  5. Napíšu test, že funkce vrací pro parametry 2 a 3 hodnotu 5.
  6. Test selže, protože funkce nic nevrací.
  7. Napíšu return 5.
  8. Test projde.
  9. Napíšu test, že funkce vrací správné výsledky i pro jiné hodnoty.
  10. Test selže, protože vrací stále samé číslo.
  11. Napíšu algoritmus pro běžná čísla.
  12. Test projde.
  13. Napíšu test na mezní hodnoty.
  14. Test selže.

Kvůli tomu často lidi na TDD zanevřou a buď testují špatně nebo vůbec. Aspoň mi to tak přijde. Přitom, pokud se to nevezme tak extrémně, je programování mnohem lepší. Mám totiž svoji verzi TDD: zůstávám u psaní testu předem, ale neiteruji tolik vášnivě. Testy spíš beru jako takový popis, co vlastně chci dělat. Dokumentaci, dá se říct. Ono pokud žádná dokumentace neexistuje, dá se právě z dobrých testů něco pořádného vyčíst. Můj styl je:

  1. Napíšu všechny testy, co mne na začátku napadnou, pro funkci add.
  2. Napíšu celou implementaci funkce add pod dozorem testů.
  3. Při psaní samotné funkce (či potom) dopíšu další testy, co mne při vývoji napadnou.

Nojo, jenže stále tam je ten test na začátku. Jak mám napsat test, pokud nevím co chci programovat? Otázka, kterou slýchám často. Na to snad jen, pokud nevím, co chci programovat, tak snad raději neprogramovat vůbec. :-)

Na test předem lze nahlížet jako na ukázku předem. Nejprve si tedy udělám kód jak chci asi danou funkci používat. Samozřejmě kód nebude funkční, jde jen o to podívat se, jak skutečně chci funkci využívat. Jakmile zjistím, jak chci funkci využívat, mohu začít psát implementaci. A pokud mám ukázku, upravit ji na test je už drobnost.

Tím si zároveň ušetříte práci. Udělat dobrý návrh na první dobrou vyjde málokdy. Proto je lepší se nejprve zamyslet nad použitím a podle toho udělat API. Jednodušší upravovat ukázku, než udělat celou implementaci a pak zjistit, že se to používá špatně a je třeba celé přepsat.

Dokážu si však představit situaci, kdy nejde napsat test dřív. Například při psaní Selenií. Nebo dělám prototyp čehokoliv a opravdu nemám představu, jaký výsledek bude, a nejde mi o to mít po celou dobu plně funkční kód (který z většiny stejně smažu). Pak je potřeba mít na paměti dvě věci:

  1. Test musím vidět selhat. Pokud napíšu test po kódu, musím kód naschvál rozbít, abych viděl, že test opravdu testuje. Nejde nijak jinak podchytit testy, co nic netestují, než ověřením, že opravdu selhávají. A že jsem už na takové narazil!
  2. Pokud programuji z nějakého důvodu bez testu, napíšu kód nejhloupěji, jak dovedu. Abych při pohledu viděl, že tam není chyba. Poté napíšu testy. Nakonec kód mohu zrefaktorovat a případně i zoptimalizovat. Nejvíce chyb totiž nastává při zhezčování kódu, refaktoringu či optimalizaci. Proto je dobré tuto fázi udělat až s testy.

To jest vše. Kolem testování se dá filosofovat nad více věcmi, ale tohle jsou nejpalčivější otázky, které dostávám a které jsem kdysi sám řešil. Snad jsem vám nějak pomohl. :-)

Python 3.0 až 3.5

Python 2.7 se hodně používá a zřejmě nějaký ten pátek ještě bude. Přechod na Python 3 se ale nevyhnutelně blíží a při takovém přechodu se skočí rovnou na nějakou nejposlednější verzi. Momentálně to může být 3.4 či dokonce před pár dny vypuštěný Python 3.5. Říkal jsem si, co vlastně všechno takového hezkého bylo přidáno od verze 3.0 do 3.5 a nebylo backportováno? Prošel jsem si všechny změny a řekl si, že se podělím o to, co mne zaujalo.

Python 3.0: Print!

print() je funkce. S tím bude těžké sžít se. První třetí verze se objevila v roce 2009 a stále jsem se nenaučil psát závorky.

Další velká změna je, že text je opravdu text a nejde mixovat s binárními daty. Takže žádné fungování při testech a poté v provozu neustálé ošetřování výjimek UnicodeDecodeError.

Všude jsou iterátory. Žádné range vs. xrange, ale pouze range vracející iterátor. Další funkce dříve vracející seznamy také jedou na iterátorech, jako třeba map, filter, zip, … Ale pozor, items, keys apod. nad slovníkem nevrací iterátor, nýbrž view. To je zase trochu něco jiného.

super() bez parametrů!

A detail možnost rozbalit tuple s hvězdičkou: (a, *rest, b) = range(5).

Python 3.1: Vůbec nic

Vše důležité bylo backportnuto i do Pythonu 2.7. Nový Python si holt teprve sedal. :-)

Python 3.2: První featury

První krůčky k asynchronnímu Pythonu, knihovna concurrent.futures.

Konečně si nebudeme muset dělat sami řešení na cachování výsledků funkcí, aneb ať žije lru_cache!

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> [fib(n) for n in range(16)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> fib.cache_info()
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

Ve functools přibyla ještě jedna zajímavost a to, že wraps navíc přidává __wrapped__ odkazující na původní funkci. Takže jde introspektnout, vrátit atp. :-)

OrderedDict dostal novou metodu move_to_end. Jen škoda, že opak udělali ošklivě parametrem…

Je možné mít configuráky s proměnnými díky ExtendedInterpolation. Mimochodem lze předat konfiguraci i jako slovník, což dodává proměnným ještě větší šťávu!

>>> parser = ConfigParser(interpolation=ExtendedInterpolation())
>>> parser.read_dict({'buildout': {'directory': '/home/ambv/zope9'}})
>>> parser.read_string("""
    [buildout]
    find-links = ${buildout:directory}/downloads/dist
    """)
>>> parser['buildout']['find-links']
'/home/ambv/zope9/downloads/dist'

Vzniknul modul argparse. Ten byl teda backportnut i do Pythonu 2.7. Ale chci se pochlubit, že existuje i argparsedialog (jen pro trojku).

Python 3.3: Rozjíždíme…

Je to tu. Tak kolik aplikací 3.3ka složí? :-) Slovník nemá definované pořadí. V dokumentaci bylo upozornění, že se nejedná o náhodné řazení, ale rozhodně se na to nemá spoléhat. Kvůli bezpečnostní chybě už nelze pořadí předvídat.

Nová syntaxe yield from pro delegování generatorů. Především pro to, co přijde v Pythonu 3.4, aby se později stalo v 2.5 zbytečným. :-)) Zkratka pro:

>>> for item in iterable:
...    yield item

>>> yield from iterable

Virtualenv už není třeba, součástí Pythonu je nový pyvenv, který řeší všechny problémy, které nemohly tooly třetích stran vyřešit na 100 procent.

__init__.py není povinný! Ano, NE-NÍ povinný. Jen otázka, zda je to dobrý nápad. Protože pak je možné naimportovat jakýkoliv adresář…

Pokud se odchytí výjimka a ta vyhodí novou výjimku, budou vidět obě. Často zbytečné, ale věřte, že v praxi se stávají i chyby při zpracování chyby, a pak je akorát původní chyba občas nedohledatelná.

>>> class C:
...     def __init__(self, extra):
...         self._extra_attributes = extra
...     def __getattr__(self, attr):
...         try:
...             return self._extra_attributes[attr]
...         except KeyError:
...             raise AttributeError(attr)
...
>>> C({}).x
Traceback (most recent call last):
  File "", line 6, in __getattr__
KeyError: 'x'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "", line 1, in 
  File "", line 8, in __getattr__
AttributeError: x

Přes atribut __qualname__ půjde nově zjistit přesně, kde byla funkce či třída definována.

>>> class C:
...     def meth(self):
...         pass
>>> C.meth.__name__
'meth'
>>> C.meth.__qualname__
'C.meth'

Vytváření souborů a vyhodit výjimku, pokud už existuje, půjde s novým módem x.

>>> open('/tmp/somefile', 'x')
<_io.TextIOWrapper name='/tmp/aaa' mode='x' encoding='ANSI_X3.4-1968'>
>>> open('/tmp/somefile', 'x')
Traceback (most recent call last):
  File "", line 1, in 
FileExistsError: [Errno 17] File exists: '/tmp/somefile'

datetime instance dostala novou metodu timestamp. Ale že to trvalo, že? :-)

>>> some_datetime.timestamp()
1442266005.577331

Nový hezký Pythonic modul ipaddress. Díky tomu lze zapsat například: (IPv4Address('127.0.0.1') + 42).is_private.

mock je součástí unittestu!

Je zpět u'' pro lepší port z dvojky! Není to jediná drobnost, která ulehčuje přechod na trojku. Takže pokud přechod, tak minimálně na… 3.4 (ne, to není překlep, jen dnes dřívější nemá moc smysl).

Python 3.4: Balíky, balíky, balíky, …

ensurepip je nový balík zajišťující přítomnost instalací Pythonu, i když se jedná o projekt mimo samotný Python. Jinými slovy pip po ruce po instalaci Pythonu!

Nový balík pro asynchronní aplikace AsyncIO. To je kde se využije delegování generátorů. AsyncIO je totiž na generátorech tak trochu postavené.

Vlastně až balík pathlib mne nahlodal projít podrobně, co vše v Pythonu 3.x bylo přidáno a upraveno.

>>> p = Path('/etc')
>>> q = p / 'init.d' / 'reboot'
>>> q
PosixPath('/etc/init.d/reboot')
>>> q.resolve()
PosixPath('/etc/rc.d/init.d/halt')

Třeba takový enum se bude taky hodit…

>>> from enum import Enum
>>> class Color(Enum):
...     red = 1
...     green = 2
...     blue = 3
>>> type(Color.red)
<enum 'Color'>
>>> isinstance(Color.green, Color)
True
>>> print(Color.red.name)
red

Nebo různé statistické metody v modulu statistics

Modul functools se rozrostl o další užitečnosti. První z nich je partialmethod, což dodává super nové možnosti:

>>> class Cell(object):
...     def __init__(self):
...         self._alive = False
...     @property
...     def alive(self):
...         return self._alive
...     def set_state(self, state):
...         self._alive = bool(state)
...     set_alive = partialmethod(set_state, True)
...     set_dead = partialmethod(set_state, False)

A pokud vám chybělo method overloading, pak to lze aspoň trochu dohnat se singledispatch. I když nedoporučoval bych takový přístup.

>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
...     if verbose:
...         print("Let me just say,", end=" ")
...     print(arg)
...
>>> @fun.register(int)
... def _(arg, verbose=False):
...     if verbose:
...         print("Strength in numbers, eh?", end=" ")
...     print(arg)
...
>>> fun("Hello, world.")
Hello, world.
>>> fun("test.", verbose=True)
Let me just say, test.
>>> fun(42, verbose=True)
Strength in numbers, eh? 42

Občas se může stát, že se do reguláru zapomene přidat dolar nebo se při nějakých změnách vytratí atp., proto další metoda fullmatch.

Zrovna nedávno jsme řešili, že naše aplikace žere docela dost paměti a občas neúměrně moc. Chtěli jsme to pořádně zanalyzovat, ale nebylo čím. Mile mne překvapil modul tracemalloc. S ním to bude sranda debugovat. :-)

Funkce min a max požírají parametr default, řešící současné vyhazování ValueError při prázdné iterable proměnné.

Python 3.5: To nejlepší nakonec

Už žádné yield from s AsyncIO. Vznik nových klíčových slov async a await. Tedy abych byl přesný, pravými klíčovými slovy budou až od Pythonu 3.7, ale je dobré se jim vyhnout už nyní.

async def read_data(db):
    data = await db.fetch('SELECT ...')

Type hinty. Druhá velká věc, o které se hodně mluví. Za mne se moc těším na možnost sem tam vynutit typ a pomoct tak i editorům v napovídání. Vlastně nemusím, jelikož se stub fily lze využít už nyní. Ale přímo v kódu je přímo v kódu. Konečně žádné cusotm dekorátory či asserty na začátku funkce.

def greeting(name: str) -> str:
    return 'Hello ' + name

Jen se mi moc nelíbí jak se budou zapisovat nějaké složitější typy s dodávanou knihovnou typing. No, diskutovalo kolem toho spoustu chytrých lidí, tak uvidíme, jaké to bude v praxi.

Krom nových klíčových slov a type hintů se dostal do Pythonu i nový operýtor @ pro násobení matic. Známá knihovna numpy přijde s podporou ve verzi 1.10. Matematici se mohou na co těšit. :-)

>>> x = numpy.ones(3)
>>> x
array([ 1., 1., 1.])

>>> m = numpy.eye(3)
>>> m
array([[ 1., 0., 0.],
       [ 0., 1., 0.],
       [ 0., 0., 1.]])

>>> x @ m
array([ 1., 1., 1.])

Kolikrát jste potřebovali zmergovat slovník a používali pro to ne úplně jasnou konstrukci dict(first, **second)? Tak nově lze pěkně hezky: {**first, **second}. Samozřejmostí je funkčnost i se seznamy, sety apod.

A to víte, že existuje možnost zabalit Pythoní aplikaci do zipu a přímo spustit? Se zipapp už ano. Zajímavá metoda pro drobnosti, které nepatří do PyPI a je zbytečné dělat jiný, třeba debianí, balík.

configparser ještě jednou. Je možné s předáním converters přidat další get* metody.

>>> parser = ConfigParser(converters={'list': [item.strip() for item in value.split() if item.strip()]})    
>>> parser.read_string("""[section]
... value = a b c d""")
>>> parser.get('section', 'value')
'a b c d'
>>> parser.getlist('section', 'value')
['a', 'b', 'c', 'd']

Pár zajímavých věcí bylo přepsáno do céčka. Například OrderedDict či lru_cache. A spoustu dalších optimalizací přes všechny releasy, jako třeba zrychlení načtení interpretu, rychlejší dumpování všeho druhu (pickle, marshall, json), rychlejší volání get property, optimalizace podmínky s výčtem (tedy lepší psát if x in {a, b, c}), …

Mne nejvíce potěšilo…

  • Vždy přítomný pip a pyvenv
  • Možnost občas hintit typy
  • Asynchronní aplikace s AsyncIO
  • Jednoduše mergovat slovníky {**first, **second}
  • Rozbalování tuplů s hvězdičkou (a, *rest, b) = range(5)
  • Možnost předat funkcím min a max defaultní hodnotu
  • Všude iterátory
  • Cachování s lru_cache
  • Práce s cestami s pathlib

Takže… taky byste nejradši hned zahodili současný codebase a použili Python 3.5? :-)

Škatulkování programátorů

Když se člověk podívá na nabídky práce, často narazí na škatulkování. Vlastně téměř pokaždé. Jednou to je Pythonista, jindy JavaScriptář, pak zase PHPčkář, C++kař, databázista a tak dále. K tomu nějaká nutná praxe. Máme to i u nás v práci.

Osobně se mi dělení nelíbí. Naopak mi přijde super mít mix lidí. Už jsem o tom nepřímo psal. Mix ve smyslu, kdy každý má jiný background. Jinou školu. Jinak přemýšlí. Má rád něco jiného. Je úplně v pořádku, když jeden zbožňuje Javu, jiný Go, další Haskell či třeba LISP. To neznamená, že všichni nemůžeme pracovat v Pythonu a přinášet super nápady odjinud.

Moc by se mi líbilo, kdyby se hledali „šikovní programátoři™“ a nijak se nedělili ani po náboru. Tým dostane několik úkolů a ať si tým úkoly rozebere sám dle libosti. Chceš teď chvíli řešit HTML5 a CSS3, aby sis osahal, co se změnilo od dob IE šestky? Chceš omrknout novinku React a ES6? Chceš zkusit ladit výkon databáze, či aplikace celkově? Chceš se naučit Python? Chceš si zkusit konfigurovat servery? Jasně, běž do toho!

Ano, někomu bez daných zkušeností to bude trvat o něco déle. Ale zase je do toho zapálen! Dělat neustále to samé člověka unavuje, až se mu do práce vůbec nechce. Pak tu je možnost párovat. Ten, kdo umí, může dělat nováčkovi v dané technologii pozorovatele a tím ho naučit. Stále to je o něco časově náročnější, ale jedná se o investici do budoucna. A když se jedná o něco, co neumí nikdo, někdo už určitě bude mít chuť se na danou věc mrknout.

Samozřejmě tím nechci říct, že všichni by měli být full-stack vývojáři. Jde mi o to, aby programátoři nebyli svázáni a mohli si vybrat úkoly dle libosti. Od client-side, přes server-side, přes mobilní aplikace až po vše, co tým vyvíjí. Pokud chce někdo zůstat u toho, co umí nejlépe, a všem tak vyhovuje, nemá smysl nijak měnit.

Takhle jsem to měl v hlavě a započal nedávno experiment. Zatím to vypadá na pozitivní reakce. Takže už nehledáme Pythonistu. Ani JavaScriptáře. Ale šikovného programátora, kterému nevadí v takovém prostředí programovat. Zbytek už naučíme. Chceš pracovat s námi? :-)