Níže uvedený text pochází z prvního vydání. Nad tímto textem se nachází aktuální stav po revizi sm>řující k druhému vydání.

Případová studie

V této případové studii rozšíříme funkčnost programu pro počítání slov, který jsme vyvinuli již dříve. Vytvoříme program, který napodobí funkci unixovského programu wc v tom smyslu, že bude vypisovat počet řádků, slov a znaků v souboru. Ale půjdeme ješt> dál a budeme vypisovat také počet v>t, klauzulí (viz poznámka dále), slov, písmen a interpunkčních znamének v textovém souboru. Vývoj programu budeme provád>t po etapách. Postupn> budeme zvyšovat jeho schopnosti. Převedeme jej do podoby modulu, abychom zvýšili jeho znovupoužitelnost. A upravíme jeho implementaci do objektov> orientované podoby, čím zvýšíme možnosti dalšího rozšiřování funkčnosti.

Implementovat jej budeme v jazyce Python, ale přinejmenším počáteční fáze mohou být napsány i v jazycích BASIC nebo Tcl. S tím, jak budeme řešit složit>jší části problému, budeme stále více používat zabudované datové struktury jazyka Python. Proto se bude obtížnost případného zápisu v jazyce BASIC zvyšovat, ačkoliv použití Tcl bude stále možné. Objektov> orientované stránky konečného řešení budou vhodné pouze pro jazyk Python.

Jako cvičení pro čtenáře bude ponechána možnost implementace dalších rysů, jako jsou:

Počítání řádků, slov a znaků

Podívejme se znovu na dříve vytvořený program pro počítání slov (viz Práce se soubory — Počítání slov):

import string
def pocetSlov(s):
    seznam = string.split(s)  
    # Poznámka překladatele: Od verze jazyka Python 2
    # je pro standardní řet>zec definována metoda split(), takže místo výše
    # uvedeného zápisu můžeme psát s.split() a není nutné provád>t import
    # modulu string.
    return len(seznam)  # vrátíme počet prvků seznamu

vstup = open("menu.txt", "r")
celkem = 0  # vytvoříme prom>nnou a nastavíme jí počáteční hodnotu nula

for radek in vstup.readlines():
    celkem = celkem + pocetSlov(radek) # sečti počty za každý řádek
print "Soubor má %d slov." % celkem

vstup.close()

Potřebujeme přidat počítadla řádků a znaků. Počítání řádků je snadné, protože cyklus zpracovává vstup po řádcích. Jednoduše přidáme n>jakou prom>nnou a budeme ji zvyšovat při každé obrátce cyklu. Počítadlo znaků je pouze mírn> složit>jší, protože můžeme procházet seznam slov a jejich délky přičítat do další prom>nné.

Rádi bychom také zvýšili obecnost použití programu tím, že jméno zkoumaného souboru zjistíme z parametru příkazového řádku nebo, pokud není zadáno, vyžádáme si zadání jména dotazem na uživatele. (Alternativní řešení by spočívalo ve čtení textu ze standardního vstupu, což práv> d>lá opravdový program wc.)

Takže konečné řešení ve stylu wc vypadá takto (poznámka překladatele: abychom se vyhnuli komplikacím s českými řet>zci ve zdrojovém textu programu, zjednodušíme si řešení tím, že použijeme cestinu bez hacku a carek):

import sys, string

# Získáme jméno souboru buď z příkazového řádku 
# nebo si je vyžádáme od uživatele.
if len(sys.argv) != 2:
    jmenoSouboru = raw_input("Zadejte jmeno souboru: ")
else:
    jmenoSouboru = sys.argv[1]
 
vstup = open(jmenoSouboru, "r")
# Poznámka překladatele: Od verze Python 2 by se m>la dávat
# přednost zápisu vstup = file(jmenoSouboru, "r")

# Počáteční hodnoty počítadel nastavíme na nuly. 
# Tím se také vytvoří příslušné prom>nné.
slov = 0
radku = 0
znaku = 0

for radek in vstup.readlines():
    radku = radku + 1
    
    # Řádek rozložíme na slova a spočítáme je.
    seznamSlov = string.split(radek)
    # Poznámka překladatele: U Python 2 lze psát
    # seznamSlov = radek.split()
    slov = slov + len(seznamSlov)
    # Počet znaků určíme z délky původního řádku, 
    # čímž započítáme mezery atd.
    znaku = znaku + len(radek)

print "%s ma %d radku, %d slov a %d znaku" % (jmenoSouboru, radku, slov, znaku)
vstup.close()

Pokud znáte unixovský příkaz wc, pak víte, že mu můžete jméno souboru zadat v podob> masky. Tím získáte hledané údaje pro všechny soubory, které masce vyhovují, a získáte také celkový součet t>chto údajů. Výše uvedený program pracuje pouze s přímo zadanými jmény souborů. Pokud chcete, aby zpracovával i soubory zadané maskou, podívejte se na modul glob. Ten vám umožní vytvořit seznam jmen vyhovujících souborů, který pak můžete zpracovat. Budete k tomu potřebovat dočasná počítadla pro každý soubor a navíc kumulativní počítadla pro celkové součty (součty součtů za jednotlivé soubory). Nebo místo toho můžete použít slovník…

Počítání v>t místo řádků

Když jsem začal přemýšlet o tom, jak bychom mohli rozšířit funkčnost, abychom počítali v>ty a slova místo "skupin znaků" (což činíme ve výše uvedeném řešení), napadlo m> nejprve, že bychom m>li ze souboru nejdříve v cyklu načíst jednotlivé řádky a pak bychom m>li v cyklu zpracovat každý řádek a získat z n>j slova do dalšího seznamu. Nakonec bychom m>li zpracovat každé "slovo" za účelem odstran>ní nadbytečných znaků.

Když jsem o tom uvažoval o n>co déle, začalo být zřejmé, že pokud bychom jednoduše shromažďovali slova a interpunkční znaménka, mohli bychom práv> interpunkční znaménka použít pro počítání v>t, klauzulí atd. (tím, že řekneme, co považujeme za v>tu nebo klauzuli s ohledem na použitá interpunkční znaménka). (Poznámka překladatele: V anglické gramatice se pojem clause (klauzule) používá ve významu v>tného členu, typicky hlavní a vedlejší v>ty. Pravidla pro výstavbu v>ty a pro psaní interpunkčních znamének jsou v anglickém jazyce mnohem propracovan>jší, takže se z nich dá strojov> lépe určit stavba v>ty. V českém jazyce je to mnohem obtížn>jší. Proto od dále uvedeného programu neočekávejte zázraky.) To znamená, že stačí, když souborem projdeme pouze jednou a poté budeme procházet přes mnohem kratší seznam interpunkčních znamének. Zkusme si to načrtnout v pseudokódu:

pro každý řádek v souboru:
    zvýšit počítadlo řádků
    if je řádek prázdný:
        zvýšit počítadlo odstavců
    rozložit řádek na skupiny znaků
    
pro každou skupinu znaků:
    zvýšit počítadlo skupin
    odstranit interpunkční znaky a přidat do slovníku - {znak: počet}
    if ve skupin> nezbyl žádný znak:
        zrušit skupinu
    else: zvýšit počítadlo slov

počet v>t = počet znaků ('.', '?', '!')
počet klauzulí = součet všech interpunkčních znaků (pon>kud ubohá definice...)

vypsat počty odstavců, řádků, v>t, klauzulí, skupin znaků, slov.
pro každý interpunkční znak:
    vypsat počet (ze slovníku)    

Vypadá to, že bychom mohli vytvořit asi 4 funkce, odpovídající výše uvedeným skupinám. To by nám pomohlo vybudovat modul, který by mohl být opakovan> použitelný buď jako celek nebo po částech.

Ud>lejme z toho modul

Klíčové funkce budou tyto: ziskejSkupinyZnaku(vstSoubor) a ziskejInerpunkci(seznamSkupin). Podívejme se, co vytvoříme na základ> uvedeného pseudokódu…


#############################
# Modul:    gramatika
# Vytvořil: A.J. Gauld, 2000,8,12
#
# Funkce:
# Počítá odstavce, řádky, v>ty, 'klauzule', skupiny znaků, slova
# a interpunkční znaménka pro textové soubory s textem předpokládajícím
# prózu. Předpokládá se, že v>ty končí znaky [.!?] a odstavce jsou odd>leny
# prázdným řádkem. Za 'klauzuli' se jednoduše považuje část v>ty, která je
# odd>lena interpunkčním znakem (což je pon>kud hloupá definice, ale jednoho
# dne můžeme doplnit n>co lepšího).
#
# Použití: Při základním použití se čte jméno souboru z parametru a vypisují
#          se všechny získané údaje. Předpokládá se vytvoření druhého modulu,
#          který by používal zde definované funkce a poskytoval užitečn>jší
#          příkazy.
#############################
import string, sys

############################
# Počáteční nastavení globálních prom>nných.
poc_odstavcu = 1 # Předpokládáme existenci nejmén> jednoho odstavce.
poc_radku, poc_vet, poc_klauzuli, poc_slov = 0, 0, 0, 0
skupiny = []
poc_interpunkcnich_znaku = {}
alfanumericke_znaky = string.letters + string.digits

# Poznámka překladatele: Pro české texty bychom mezi 
# alfanumerické znaky m>li přidat i znaky s diakritickými znaménky. V>c se 
# ale komplikuje tím, že bychom navíc m>li uvažovat i způsob kódování
# vstupního souboru. Obecné řešení by nebylo úpln> jednoduché, takže zatím
# nebudeme překlad originálního zdrojového textu z angličtiny upravovat.

koncove_znaky = ['.', '?', '!']
interpunkcni_znaky = ['&', '(', ')', '-', ';', ':', ','] + koncove_znaky
for c in interpunkcni_znaky:
    poc_interpunkcnich_znaku[c] = 0
format = """%s obsahuje:
%d odstavcu, %d radku a %d vet.
Ty obsahuji %d klauzuli a celkem %d slov."""


############################
# Nyni nadefinujeme funkce, které tvoří jádro činnosti.

def ziskejSkupinyZnaku(vstSoubor):
    pass

def ziskejInerpunkci(seznamSkupin):
    pass

def vypsatStatistiky():
    print format % (sys.argv[1], poc_odstavcu, 
                    poc_radku, poc_vet,
                    poc_klauzuli, poc_slov)

def Analyzuj(vstSoubor):
    ziskejSkupinyZnaku(vstSoubor)
    ziskejInerpunkci(skupiny)
    vypsatStatistiky()


# Pokud je modul volán z příkazového řádku, zajisti spušt>ní následujícího
# kódu. (V tomto případ> je magická prom>nná __name__ nastavena na hodnotu
# "__main__".)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print "Pouziti: python gramatika.py <soubor>"
        sys.exit()
    else:
        Dokument = open(sys.argv[1], "r")
        # Poznámka překladatele: Od verze Python 2 by se m>la dávat
        # přednost zápisu Dokument = file(sys.argv[1], "r")  
        Analyzuj(Dokument)
        Dokument.close()

V tomto míst> jsem necht>l ukázat celé řešení v podob> jednoho dlouhého výpisu. Proberu rad>ji uvedenou kostru a potom se podíváme na každou z uvedených tří významných funkcí. Nicmén> k tomu, abyste program uvedli do chodu, budete muset na konci vše slepit dohromady.

První v>cí, která stojí za povšimnutí, je komentář na začátku. Jeho uvedení patří k b>žným praktikám. Má čtenáři naznačit, co soubor obsahuje a jak má být používán. Užitečná je rovn>ž informace o verzi (autor a datum) a to zejména pro n>koho, kdo již možná používá nov>jší nebo starší verzi.

Poslední úsek souvisí s rysem systému Python, který každému modulu spušt>nému z příkazového řádku vnitřn> říká "__main__" (čti mein; hlavní). Můžeme si otestovat obsah speciální zabudované prom>nné __name__ (čti nejm; jméno). Pokud je v ní uveden zmín>ný řet>zec, pak víme, že modul nebyl importován, ale že byl spušt>n. Takže v takovém případ> můžeme provést spoušt>cí kód, který je uveden v t>le příkazu if.

Náš spoušt>cí kód obsahuje uživatelsky přív>tivou nápov>du o způsobu spušt>ní programu pro případ, kdybychom jej spustili bez zadání jména souboru, nebo kdybychom naopak uvedli příliš mnoho jmen souborů.

Na záv>r poznamenejme, že funkce Analyzuj() jednoduše zavolá ostatní funkce ve správném pořadí. K b>žným praktikám patří op>t to, že si uživatel může vybrat, zda bude chtít používat celkovou funkčnost přímočarým způsobem (voláním funkce Analyzuj()) nebo zda bude přímo volat nízkoúrovňové, primitivní funkce.

ziskejSkupinyZnaku()

Pseudokód pro tento úsek vypadal následovn>:

pro každý řádek v souboru:
    zvýšit počítadlo řádků
    if je řádek prázdný:
        zvýšit počítadlo odstavců
    rozložit řádek na skupiny znaků

V jazyce Python to můžeme implementovat velmi snadno:


# Použijeme globální prom>nné počítadel a globální seznam skupin znaků.
def ziskejSkupinyZnaku(vstSoubor):
    global poc_odstavcu, poc_radku, skupiny
    try:
        for radek in vstSoubor.readlines():
            poc_radku = poc_radku + 1
            if len(radek) == 1: # prázdný řádek => odd>lení odstavce
                poc_odstavcu = poc_odstavcu + 1
            else:
                skupiny = skupiny + string.split(radek)
                # Poznámka překladatele: Od verze Python 2 lze psát
                # skupiny = skupiny + radek.split()
    except:
        print "Nepodarilo se cist ze souboru ", sys.argv[1]
        sys.exit()

Poznámka 1: Abychom oznámili, že budeme používat prom>nné, které byly vytvořeny vn> t>la funkce, musíme použít klíčové slovo global. Pokud bychom tak neučinili, pak by při přiřazování do nich Python vytvořil nové prom>nné se stejnými jmény, které by ovšem byly lokální uvnitř t>la funkce. Zm>ny obsahu takových lokálních prom>nných by se u globálních prom>nných (na úrovni modulu) neprojevily.

Poznámka 2: K případnému oznámení chyb při čtení souboru a ukončení b>hu programu jsme použili konstrukci try/except.

ziskejInerpunkci()

Zde budeme muset vyvinout o n>co v>tší úsilí. Použijeme také pár nových rysů jazyka Python. Příslušný pseudokód vypadal následovn>:

pro každou skupinu znaků:
    zvýšit počítadlo skupin
    odstranit interpunkční znaky a přidat do slovníku - {znak: počet}
    if ve skupin> nezbyl žádný znak:
        zrušit skupinu
    else: zvýšit počítadlo slov

Můj první pokus vypadal n>jak takto:

def ziskejInerpunkci(seznamSkupin):
    global poc_interpunkcnich_znaku
    for skupina in seznamSkupin:
        while skupina and (skupina[-1] not in alfanumericke_znaky):
            p = skupina[-1]
            skupina = skupina[:-1]
            if p in poc_interpunkcnich_znaku.keys():
                 poc_interpunkcnich_znaku[p] = poc_interpunkcnich_znaku[p] + 1
            else: poc_interpunkcnich_znaku[p] = 1

Povšimn>te si, že tato verze nezahrnuje záv>rečnou konstrukci if/else, kterou obsahoval pseudokód. Vynechal jsem ji kvůli zjednodušení. M>l jsem také pocit, že se v praktických případech objeví velmi málo skupin, které obsahují pouze interpunkční znaky. Ale do konečné verze kódu tuto konstrukci doplníme.

Poznámka 1: Seznam skupin jsme se rozhodli předávat parametrem, takže uživatelé tohoto modulu mohou předat svůj vlastní seznam místo toho, aby byli nuceni pracovat se souborem.

Poznámka 2: Za zmínku stojí obrat, kdy jsme prom>nné skupina přiřadili hodnotu skupina[:-1]. V jazyce Python je tento obrat znám jako slicing (čti slajsing; odřezávání, odkrajování). Dvojtečka říká, aby byl index chápán jako rozsah. Pokud bychom například cht>li získat seznam položek skupina[3], skupina[4] a skupina[5], vyjádřili bychom jej jako skupina[3:6]. (Poznámka překladatele: tento obrat lze používat pro posloupnosti různých druhů, konkrétn> pro řet>zce a pro seznamy.)

Pokud neuvedeme číslo, pak se to chápe jako začátek nebo konec seznamu podle toho, na které stran> dvojtečky necháme prázdné místo. Takže zápis skupina[3:] by m>l význam všech členů skupina od skupina[3:] až do konce. Jde o jeden z velmi užitečných rysů jazyka Python. V našem příkladu je originální posloupnost skupina ztracena (a tím pádem náležit> uklizena, uvoln>na). Nov> vytvořená posloupnost (v našem případ> řet>zec) je přiřazena do skupina.

Poznámka 3: Pro získání posledního znaku z prom>nné skupina používáme záporný index. Jde op>t o velmi užitečný rys jazyka Python. Pro případ, že by se na konci skupiny objevilo více interpunkčních znaků, provádíme zpracování v cyklu.

B>hem testů se ukázalo, že totéž musíme provád>t i pro začátek skupiny, protože, ačkoliv uzavírací závorky detekovány jsou, otvírací závorky nikoliv. Pro vyřešení tohoto problému vytvoříme novou funkci trim(), která odstraní interpunkční znaky ze začátku a z konce jedné skupiny znaků:


#########################################################
# Poznámka: trim používá rekurzi, kde podmínkou ukončení je buď hodnota
# 0 nebo -1. Pokud se objeví n>co jiného, než hodnoty -1, 0 nebo 2,
# je vyvolána chyba "InvalidEnd". 
# Poznámka překladatele: Od verze Python 2.0 se doporučuje pro výjimky
# používat instance tříd odvozených od třídy Exception. Používání
# řet>zců pro výjimky se již nedoporučuje.
##########################################################
def trim(polozka, end = 2):
  """ Odstraní nealfanumerické znaky zleva (end = 0), zprava (-1) nebo 
      z obou stran prom>nné polozka."""

  if end not in [0, -1, 2]:
     raise "InvalidEnd"

  if end == 2:
     trim(polozka, 0)
     trim(polozka, -1)
  else:
     while (len(polozka) > 0) and (polozka[end] not in alfanumericke_znaky):
        ch = polozka[end]
        if ch in poc_interpunkcnich_znaku.keys():
           poc_interpunkcnich_znaku[ch] = poc_interpunkcnich_znaku[ch] + 1
        if end == 0: polozka = polozka[1:]
        if end == -1: polozka = polozka[:-1]

Povšimn>te si, jak nám kombinace použití rekurze a implicitní (přednastavené) hodnoty parametru umožnila definovat jedinou funkci trim, která standardn> ošetří oba konce. Pokud ale zadáme hodnotu parametru end, můžeme ji použít k ošetření pouze jednoho konce. Hodnoty parametru end jsou zvoleny tak, aby odpovídaly způsobu indexování v jazyce Python: nula pro levou stranu a -1 pro pravou. Původn> jsem vytvořil dv> funkce trim, jednu pro každý konec. Ale díky množství pozorovaných podobností jsem zjistil, že je při použití parametru mohu zkombinovat dohromady.

Funkce ziskejInerpunkci se tím velmi zjednoduší:

def ziskejInerpunkci(seznamSkupin):
    for skupina in seznamSkupin:
        trim(skupina)
    # Nyní vypustíme prázdná 'slova'.
    for i in range(len(seznamSkupin)):
        if len(seznamSkupin[i]) == 0:
            del(seznamSkupin[i])

Poznámka 1: Nová implementace navíc provádí vypoušt>ní prázdných slov.

Poznámka 2: V zájmu znovupoužitelnosti by možná bývalo bylo lepší, kdybychom funkci trim rozbili na ješt> menší kousky. To by nám umožnilo vytvořit funkci pro odstran>ní jediného interpunkčního znaménka — buď ze začátku, nebo z konce slova — a odstran>ný znak bychom mohli vracet jako výsledek. Takovou funkci by pak mohla opakovan> volat jiná funkce, čímž bychom získali požadovaný výsledek. Ale náš modul se má zabývat zjišťováním konkrétních statistických údajů ze zadaného textu a ne n>jakým obecným zpracováním textu. Pro uvedené funkce bychom tedy správn> m>li vytvořit samostatný modul, který bychom pak importovali. Jenže ten by obsahoval jen jednu funkci, která se navíc nezdá být příliš užitečnou. Takže to rad>ji nechejme, jak to je.

Konečná podoba modulu gramatika

Teď už nám zbývá jenom vylepšit výpis výsledků tím, že do n>j zahrneme další počítadla a vliv interpunkčních znaků. Existující funkci vypsatStatistiky() nahraďte následujícím kódem:

def vypsatStatistiky():
    global poc_vet, poc_klauzuli
    for c in koncove_znaky:
        poc_vet = poc_vet + poc_interpunkcnich_znaku[c]
    for c in poc_interpunkcnich_znaku.keys():
        poc_klauzuli = poc_klauzuli + poc_interpunkcnich_znaku[c]
    print format % (sys.argv[1], poc_odstavcu, 
                    poc_radku, poc_vet,
                    poc_klauzuli, poc_slov)
    print "Byly pouzity nasledujici interpunkcni znaky:"
    for c in poc_interpunkcnich_znaku.keys():
        print "\t%s\t:\t%3d" % (c, poc_interpunkcnich_znaku[c])

Pokud jste pečliv> vložili všechny výše uvedené funkce na správná místa, m>li byste nyní po napsání

C:> python gramatika.py mujsoubor.txt

obdržet výpis statistických údajů pro váš soubor mujsoubor.txt (ať už jej nazvete jak chcete). Užitečnost tohoto programu je diskutabilní, ale snad vám sledování vývoje jeho kódu pomohlo získat představu o tom, jak můžete tvořit své vlastní programy. Za hlavní považuji to, abyste si vše zkoušeli. A ovšem, m>li byste si je také pečliv> otestovat. V případ> tohoto programu například rychle zjistíte způsoby, jak z n>j vylákat falešné odpov>di. Pokud například v>tu zakončíte třemi tečkami, bude počítadlo v>t nabývat příliš vysoké hodnoty. Pro rozpoznání takových situací můžete doplnit k tomu určený kód. Můžete se také rozhodnout, že s ohledem na občasné použití programu vám podobné v>ci nevadí. Je to na vás.

Nepovažuji za žádnou ostudu, když si vyzkoušíte n>kolik různých přístupů. Často b>hem toho získáte cenné zkušenosti.

Náš kurs ukončíme přepracováním modulu gramatika na použití technik objektov> orientovaného programování. B>hem tohoto procesu uvidíte, že objektov> orienovaný přístup vede k modulům, které jsou pro koncového uživatele ješt> pružn>jší a také rozšířiteln>jší.

Třídy a objekty

Jeden z nejv>tších problémů, se kterým se uživatel našeho modulu setká, spočívá ve spoléhání se na globální prom>nné. Vede to k tomu, že můžeme analyzovat vždy jen jeden dokument najednou. Jakýkoliv pokus o zpracování více dokumentů by vedl k přepisování hodnot globálních prom>nných.

Pokud přeneseme tyto globální údaje dovnitř třídy, můžeme vytvořit n>kolik instancí dané třídy (pro každý soubor jednu). Každá instance tím získá svou vlastní sadu prom>nných. Když navíc metody třídy dostatečn> rozčleníme, můžeme vytvořit architekturu, u které bude moci tvůrce objektu pro nový typ dokumentu snadno upravit kritéria tak, aby vyhovovala novým pravidlům (ze seznamu slov můžeme například vyloučit všechny HTML značky).

Náš první pokus vypadá takto:

#! /usr/local/bin/python
################################
# Modul: dokument.py
# Autor: A.J. Gauld
# Datum: 2000/08/12
# Verze: 2.0
################################
# Tento modul poskytuje třídu Dokument, ze které
# lze odvozovat další třídy pro různé kategorie
# dokumentů (text, HTML, LaTeX, atd.). Jako vzor
# jsou uvedeny třídy pro text a HTML.
#
# K nejdůležit>jším službám patří
# - ziskejSkupinyZnaku(),
# - ziskejSlova(),
# - vypsatStatistiky().
################################
import sys, string

class Dokument:
    def __init__(self, jmenoSouboru):
        self.jmenoSouboru = jmenoSouboru
        self.poc_odstavcu = 1
        self.poc_radku, self.poc_vet = 0, 0
        self.poc_klauzuli, self.poc_slov = 0, 0
        self.alfanum = string.letters + string.digits
        self.koncove_znaky = ['.','?','!']
        self.interpunkcni_znaky = ['&', '(', ')', '-', ';', 
                                   ':', ','] + self.koncove_znaky
        self.radky = []
        self.skupiny = []
        self.poc_interp_znaku = {}
        for c in self.interpunkcni_znaky + self.koncove_znaky:
           self.poc_interp_znaku[c] = 0
        self.format = """%s obsahuje:
%d odstavcu, %d radku a %d vet.
Ty zase obsahuji %d klauzuli a celkem %d slov."""

    def nactiRadky(self):
        try:
            self.vstSoubor = open(self.jmenoSouboru, "r")
            # Poznámka překladatele: Od verze Python 2 by se 
            # m>la dávat přednost zápisu 
            # self.vstSoubor = file(self.jmenoSouboru, "r")
            self.radky = self.vstSoubor.readlines()
            self.vstSoubor.close()
        except:
            print "Chyba pri cteni ze souboru ", self.jmenoSouboru
            sys.exit()
  
    def ziskejSkupinyZnaku(self, radky):
        for radek in radky:
            radek = radek[:-1]  # odstraníme koncový '\n'
            self.poc_radku = self.poc_radku + 1
            if len(radek) == 0: # prázdný => další odstavec
                self.poc_odstavcu = self.poc_odstavcu + 1
            else:
                self.skupiny = self.skupiny + string.split(radek)
                # Poznámka překladatele: Od verze Python 2 lze psát
                # self.skupiny = self.skupiny + radek.split()
  
  
    def ziskejSlova(self):
        pass
  
    def vypsatStatistiky(self, odstavcu=1, radku=1, vet=1, slov=1, interpun=1):
        pass
  
    def Analyzuj(self):
        self.nactiRadky()
        self.ziskejSkupinyZnaku(self.radky)
        self.ziskejSlova()
        self.vypsatStatistiky()

class TextovyDokument(Dokument):
    pass

class HTMLDokument(Dokument):
    pass

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print "Pouziti: python dokument.py <jmeno souboru>"
        sys.exit()
    else:
        D = Dokument(sys.argv[1])
        D.Analyzuj()

Následujícím krokem při implementaci této třídy je definice metody ziskejSlova. Mohli bychom jednoduše okopírovat to, co jsme vytvořili v předchozí verzi a vytvořit n>jakou metodu trim. Jenže my chceme, aby byla objektov> orientovaná verze snadno rozšiřitelná. Takže místo toho rozd>líme metodu ziskejSlova na posloupnost n>kolika kroků. V odvozených třídách pak stačí přepsat jen nové verze t>chto podkroků a nikoliv celou metodu ziskejSlova. To by m>lo usnadnit zpracování mnohem širšího rozsahu typů dokumentů.

Konkrétn> přidáme metodu pro odmítnutí skupin, které budou rozpoznány jako chybné, metodu pro odstran>ní necht>ných znaků ze začátku a z konce. To znamená, že do třídy Dokument přidáme tři metody a metodu ziskejSlova implementujeme pomocí nich.

class Dokument:
    # ... viz výše
    def ziskejSlova(self):
        for w in self.skupiny:
            self.orezatLeve(w)
            self.orezatPrave(w)
        self.odstranitVyjimky()
  
    def odstranitVyjimky(self):
        pass
  
    def orezatLeve(self, slovo):
        pass
  
    def orezatPrave(self, slovo):
        pass

Povšimn>te si, že t>la uvedených funkcí definují použití jediného příkazu pass (čti pás; projít), který ned>lá vůbec nic. Místo n>j budeme pozd>ji definovat způsob zpracování pro každý konkrétní typ dokumentu.

Textový dokument

Třída pro textový dokument vypadá takto:

class TextovyDokument(Dokument):
    def orezatLeve(self, slovo):
        while (len(slovo) > 0) and (slovo[0] not in self.alfanum):
            ch = slovo[0]
            if ch in self.poc_interp_znaku.keys():
                self.poc_interp_znaku[ch] = self.poc_interp_znaku[ch] + 1
            slovo = slovo[1:]
        return slovo

    def orezatPrave(self, slovo):
        while (len(slovo) > 0) and (slovo[-1] not in self.alfanum):
            ch = slovo[-1]
            if ch in self.poc_interp_znaku.keys():
                self.poc_interp_znaku[ch] = self.poc_interp_znaku[ch] + 1
            slovo = slovo[:-1]
        return slovo
      
    def odstranitVyjimky(self):
        self.skupiny = filter(lambda g: len(g) > 0, self.skupiny)

Ořezávací funkce (anglicky se tato funkčnost označuje slovem trim) jsou v podstat> shodné s funkcí trim v našem modulu gramatika.py, která byla ovšem rozd>lena na dv>. Funkce odstranitVyjimky byla definována tak, aby odstraňovala prázdná slova. Povšimn>te si použití funkce filter(), o které jsme se zmínili v části v>nované funkcionálnímu programování.

HTML dokument

Při zpracování HTML dokumentů použijeme funkčnost jazyka Python, se kterou jsme se ješt> nesetkali — regulární výrazy. Jde o speciální řet>zcové vzorky, které se dají použít pro nalezení složit>jších řet>zců. Použijeme je zde k odstran>ní čehokoliv mezi znaky < a >. To znamená, že budeme muset předefinovat metodu ziskejSlova. Odstraňování interpunkčních znamének by m>lo být shodné jako v případ> zpracování holého textu. Takže místo abychom d>dili přímo z třídy Dokument, odvodíme novou třídu z TextovyDokument a použijeme jí definované metody pro ořezávání.

Takže třída HTMLDokument bude vypadat takto:

class HTMLDokument(TextovyDokument):
    def odstranitVyjimky(self):
        """ Použijeme regulární výrazy pro odstran>ní všech <.+?>"""
        import re
        tag = re.compile("<.+?>")# použijeme non greedy re
        L = 0
        while L < len(self.radky):
            if len(self.radky[L]) > 1: # pokud řádek není prázdný
                 self.radky[L] = tag.sub('', self.radky[L])
                 if len(self.radky[L]) == 1:
                     del(self.radky[L])
                 else: L = L + 1
            else: L = L + 1


    def ziskejSlova(self):
        self.odstranitVyjimky()
        for i in range(len(self.skupiny)):
             slovo = self.skupiny[i]
             slovo = self.orezatLeve(slovo)
             self.skupiny[i] = self.orezatPrave(slovo)
        TextovyDokument.odstranitVyjimky(self)# odstraní prázdná slova

Poznámka: V tomto míst> stojí za zmínku pouze volání self.odstranitVyjimky před ořezáváním a poté volání TextovyDokument.odstranitVyjimky. Pokud bychom se spoléhali na zd>d>nou metodu ziskejSlova, byla by po provedení ořezání volána naše metoda odstranitVyjimky, což nechceme.

Poznámka překladatele: Pojem greedy (čti grídy), který se objevil v poznámce u regulárního výrazu, odpovídá významu anglického slovíčka — chamtivý, lakomý, žravý. Vzorek <.+>, který bychom použili pro regulární výraz lze číst jako řet>zec, který začíná levou úhlovou závorkou (znak 'menší'), pokračuje jedním a více (znak plus) libovolných znaků (znak tečka) a končí pravou úhlovou závorkou (znak 'v>tší'). Pokud neurčíme jinak, snaží se Python najít odpovídající řet>zec, který je co nejv>tší (tj. greedy, neboli žravé chování). To ovšem znamená, že by se k prvnímu znaku 'menší' na daném řádku nalezl až poslední znak 'v>tší'. Při přeskakování libovolných znaků by mohly být přeskočeny i znaky menší/v>tší, které se nacházejí mezi nimi. My ovšem potřebujeme, aby se přeskakování zastavilo na nejbližším znaku 'v>tší', který ukončuje HTML značku. To znamená, že chceme předepsat opačné chování (tj. non greedy), kdy se nalezne co nejkratší vyhovující řet>zec. V jazyce Python tento požadavek vyjádříme tím, že za popis skupiny doplníme otazník. Použijeme tedy vzorek <.+?>.

Přidáme grafické uživatelské rozhraní

Pro vytvoření grafického uživatelského rozhraní použijeme Tkinter, který jsme stručn> představili v kapitole Událostmi řízené programování a kterému jsme se v>novali podrobn>ji v rámci tématu v>novanému grafickému uživatelskému rozhraní. Tentokrát vytvoříme o n>co propracovan>jší grafické uživatelské rozhraní a použijeme více ovládacích prvků, zvaných také widget, které nám Tkinter poskytuje.

Refaktorizace[1] třídy Dokument

Dříve než se do tohoto stadia dostaneme, musíme upravit naši třídu Dokument. Dosavadní verze provádí zobrazení výsledků tiskem na standardní výstup (stdout) v rámci metody Analyzuj. Při vytváření grafického uživatelského rozhraní to ale není to pravé. Místo toho bychom rad>ji přivítali, kdyby metoda Analyzuj jednoduše uložila výsledky v atributech s charakterem počítadel, ke kterým bychom pak přistupovali podle potřeby. Dosáhneme toho jednoduše rozd>lením nebo refaktorizací metody vypsatStatistiky() na dv> části: metodu vypocetStatistik(), která vyhodnotí výsledky a uloží je v počítadlech, a na metodu tiskStatistik(), která vytiskne výsledky na standardní výstup.

Nakonec musíme upravit metodu Analyzuj() tak, aby volala metodu vypocetStatistik(), a hlavní sekvenci příkazů tak, aby po volání metody Analyzuj() volala tiskStatistik(). Po provedení t>chto úprav bude stávající kód pracovat stejným způsobem, jako předtím — přinejmenším z pohledu uživatele, který takový program spouští z příkazového řádku. Ostatní uživatelé (tj. ti, kteří zdrojový soubor používají jako modul, vytvářejí si sami instance třídy Dokument a volají sami jeho metody) budou muset provést ve svém kódu drobné zm>ny, kdy po použití metody Analyzuj() zavolají metodu tiskStatistik() — a to není příliš obtížné.

Revidované úseky kódu vypadají takto:

    def vypocetStatistik(self):
        self.poc_slov = len(self.skupiny)
        for c in self.koncove_znaky:
            self.poc_vet = self.poc_vet + self.poc_interp_znaku[c]
        for c in self.poc_interp_znaku.keys():
            self.poc_klauzuli = self.poc_klauzuli + self.poc_interp_znaku[c]

    def tiskStatistik(self):
        print self.format % (self.jmenoSouboru, self.poc_odstavcu, 
                             self.poc_radku, self.poc_vet,
                             self.poc_klauzuli, self.poc_slov)
        print "Byly pouzity nasledujici interpunkcni znaky:"
        for c in self.poc_interp_znaku.keys():
            print "\t%s\t:\t%4d" % (c, self.poc_interp_znaku[c])

a t>lo provád>né při spušt>ní z příkazového řádku:

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print "Pouziti: python dokument.py <jmeno souboru>"
        sys.exit()
    else:
        try:
            D = HTMLDokument(sys.argv[1])
            D.Analyzuj()
            D.tiskStatistik()
        except:
            print "Chyba pri analyze souboru: %s" % sys.argv[1]

Nyní jsme připraveni k tomu, abychom naše třídy dokumentů obalili grafickým uživatelským rozhraním.

Návrh grafického uživatelského rozhraní

V prvním kroku si pokusíme představit, jak to vše bude vypadat. Musíme zadat jméno souboru, takže budeme potřebovat ovládací prvky Edit nebo Entry. Musíme určit, zda chceme provád>t analýzu holého textu nebo obsahu v podob> HTML. Takový způsob výb>ru jedné z n>kolika možností je obvykle reprezentován sadou prvků typu Radiobutton. Tyto ovládací prvky by m>ly být sdruženy dohromady, aby bylo vid>t, že spolu souvisejí.

Dále požadujeme n>jaký způsob zobrazení výsledků. Mohli bychom použít n>kolik prvků typu Label — jeden pro každé počítadlo. Místo toho použiji jednoduchý prvek typu Text, do kterého můžeme vkládat řet>zce. Tento přístup se přibližuje duchu dřív>jšího řádkového výstupu, ale konkrétní způsob výstupu je v>cí volby návrháře.

Na záv>r, potřebujeme n>jaké prostředky pro zahájení analýzy a pro ukončení aplikace. Protože pro zobrazení výsledků použijeme ovládací prvek typu text, mohlo by být užitečné, kdybychom mohli do počátečního stavu uvést i zobrazování. Všechny uvedené příkazy mohou být vyjádřeny prvky typu Button (tlačítko).

Pokud si načrtneme podobu odpovídajícího grafického uživatelského rozhraní, dostaneme n>co takového:

+-------------------------+-----------+
|    Jméno souboru        | (*) text  |
|                         | ( ) HTML  |
+-------------------------+-----------+
|                                     |
|                                     |
|                                     |
|                                     |
|                                     |
+-------------------------------------+
|                                     |
|   Analyzuj      Vymazat     Konec   |
|                                     |
+-------------------------------------+

Teď můžeme přistoupit k psaní kódu — krok po kroku:

from Tkinter import *
import dokument

################### Definice tříd ######################
class AplikaceGramatika(Frame):
    def __init__(self, rodic=0):
        Frame.__init__(self, rodic)
        self.typ = 2 # vytvoř prom>nnou s počáteční hodnotou
        self.master.title('Pocitadlo gramatickych prvku')
        self.vybudovatUI()

Nejdříve jsme provedli import modulů Tkinter a dokument. V prvním případ> jsme si v rámci vytvářeného modulu zajistili viditelnost všech jmen z Tkinter, zatímco v druhém případ> budeme muset před jména přidávat předponu 'dokument'.

Definovali jsme i metodu __init__, která volá metodu Frame.__init__ své bázové třídy. Tím se zajistí správná vnitřní nastavení v rámci Tkinter. Poté vytváříme atribut, ve kterém bude uložena hodnota typu dokumentu. A nakonec voláme metodu vybudovatUI, která nám vytvoří všechny potřebné ovládací prvky.

    def vybudovatUI(self):
        # Informace o souboru: Jméno a typ
        fSoubor = Frame(self)
        Label(fSoubor, text="Jmeno souboru: ").pack(side=LEFT)
        self.eJmeno = Entry(fSoubor)
        self.eJmeno.insert(INSERT, "test.htm")
        self.eJmeno.pack(side=LEFT, padx=5)
        
        
        # Pro zajišt>ní zarovnání přepínacích tlačítek (radio buttons)
        # se jménem potřebujeme další rámec.
        fTyp = Frame(fSoubor, borderwidth=1, relief=SUNKEN)
        self.rText = Radiobutton(fTyp, text="text",
                                 variable = self.typ, value=2, 
                                 command=self.udalostText)
        self.rText.pack(side=TOP, anchor=W)
        self.rHTML = Radiobutton(fTyp, text="HTML",
                                 variable=self.typ, value=1,
                                 command=self.udalostHTML)
        self.rHTML.pack(side=TOP, anchor=W)
        # Na počátku vybereme 'text'
        self.rText.select()
        fTyp.pack(side=RIGHT, padx=3)
        fSoubor.pack(side=TOP, fill=X)
        
        
        # V textovém okn> se zobrazuje výstup. Použijeme vycpávku, abychom
        # získali rámeček. Rodičovským rámcem bude rámec celé aplikace
        # (tj. self)
        self.txtBox = Text(self, width=60, height=10)
        self.txtBox.pack(side=TOP, padx=3, pady=3)
        
        
        # Nakonec umístíme příkazová tlačítka, která budou spoušt>t činnosti.
        fTlacitka = Frame(self)
        self.bAnalyzuj = Button(fTlacitka, text="Analyzuj",
                                command=self.udalostAnalyzuj)
        self.bAnalyzuj.pack(side=LEFT, anchor=W, padx=50, pady=2)
        self.bReset = Button(fTlacitka, text="Reset",
                             command=self.udalostReset)
        self.bReset.pack(side=LEFT, padx=10)
        self.bKonec = Button(fTlacitka, text="Konec",
                             command=self.udalostUkonceni)
        self.bKonec.pack(side=RIGHT, anchor=E, padx=50, pady=2)
        
        fTlacitka.pack(side=BOTTOM, fill=X)
        self.pack()

Nebudu zde vysv>tlovat všechny detaily. Místo toho vám doporučuji k nahlédnutí učebnici Tkinter, kterou naleznete na webových stránkách jazyka Python. Jde o vynikající úvod i referenční příručku k Tkinter. Obecný princip uvedeného kódu spočívá ve vytváření ovládacích prvků odpovídajících tříd, při kterém zadáváme nastavení formou pojmenovaných parametrů. Poté je příslušný prvek umíst>n do svého obklopujícího rámce voláním metody pack.

Poznamenejme, že k dalším klíčovým bodům patří použití pomocných prvků typu Frame, které sdružují přepínací a příkazová tlačítka. U přepínacích tlačítek (radio buttons) se mimo jiné uvádí dvojice parametrů s názvy variable (prom>nná) a value (hodnota). První z uvedených svazuje přepínací tlačítka dohromady tím, že udává stejnou vn>jší prom>nnou (self.typ). Druhý parametr přid>luje každému přepínacímu tlačítku jednoznačnou hodnotu. Všimn>te si také parametru command=xxx, který se předává prvkům tlačítek. Jde o metody, které bude Tkinter volat v okamžiku stisku tlačítka. Jejich kód je uveden níže:

    
    ################# Metody pro ošetření událostí ####################
    # je načase vše skoncovat...
    def udalostUkonceni(self):
        import sys
        sys.exit()
    
    
    # nastavíme vše do počátečního stavu
    def udalostReset(self):
        self.txtBox.delete(1.0, END)
        self.rText.select()
    
    
    # nastavíme hodnotu přepínacího tlačítka
    def udalostText(self):
        self.typ = 2
    
    def udalostHTML(self):
        self.typ = 1

Uvedené metody jsou velmi jednoduché a doufám, že není nutné je vysv>tlovat. Poslední metoda pro ošetření události zajišťuje provedení analýzy:

    
    # Vytvoříme odpovídající typ dokumentu a provedeme analýzu.
    # Poté zobrazíme výsledky v podob> řet>zců.
    def udalostAnalyzuj(self):
        jmenoSouboru = self.eJmeno.get()
        if jmenoSouboru == "":
            self.txtBox.insert(END,"\nNo filename provided!\n")
            return
        if self.typ == 2:
            doc = dokument.TextovyDokument(jmenoSouboru)
        else:
            doc = dokument.HTMLDokument(jmenoSouboru)
        self.txtBox.insert(END, "\nAnalyzuji...\n")
        doc.Analyzuj()
        str = doc.format % (doc.jmenoSouboru,
                            doc.poc_odstavcu, doc.poc_radku,
                            doc.poc_vet, doc.poc_klauzuli, doc.poc_slov)
        self.txtBox.insert(END, str)

I výše uvedený text byste již m>li být schopni přečíst a pochopit, co d>lá. Jeho klíčové body jsou následující:

Teď už nám zbývá jen vytvořit hlavní objekt aplikace a spustit smyčku pro zpracování událostí:

mojeAplikace = AplikaceGramatika()
mojeAplikace.mainloop()

Podívejme se, jak vypadá konečný výsledek při spušt>ní v systému MS Windows. Zobrazeny jsou výsledky analýzy testovacího HTML souboru; nejdříve v režimu text a poté v režimu HTML:

Konečný vzhled aplikace.

A je to. Pokud chcete, můžete pokračovat ve zdokonalování zpracování HTML. Můžete vytvořit nové moduly pro nové typy dokumentů. Můžete zkusit zam>nit okno s textem za n>kolik prvků s popisným textem vložených do rámce. Ale z pohledu našeho původního zám>ru jsme hotovi. Následující kapitola nabízí nám>ty k dalšímu studiu v závislosti na vašich programátorských tužbách. Hlavní je, aby vás to bavilo. A vždycky si pamatujte: Počítač je hloupý!


Pokud vás napadne, co by se dalo na překladu této kapitoly vylepšit, zašlete e-mail odklepnutím Tím budou do dopisu automaticky vloženy informace o tomto HTML dokumentu.

$Id: cztutcase.html,v 1.7 2005/10/07 19:07:04 petr Exp $