Differences

This shows you the differences between two versions of the page.

Link to this comparison view

courses:b4b33rph:cviceni:program_po_tydnech:tyden_09 [2018/12/04 16:25] (current)
Line 1: Line 1:
 +====== Spam II ======
 +
 +  * Dotazy a odpovědi
 +  * Testík
 +  * Diskuse DÚ: confusion matrix, funkce kvality
 +  * Interaktivní programování:​ ''​BinaryConfusionMatrix''​ metodou TDD
 +  * Diskuse: jak dokončit krok 3 spam filtru? Co se bude odevzdávat?​
 +  * Hádanka
 +  * Diskuse DÚ: jednoduché filtry
 +===== Dotazy a odpovědi =====
 +
 +===== Programovací testík =====
 +Zadání na interních stránkách.
 +
 +
 +===== Diskuse =====
 +  * Jak hodnotit kvalitu filtru? Co jsou to TP, FP, TN, FN? 
 +
 +> {{page>​..:​..:​internal:​cviceni:​spam:​krok3#​matice_zamen&​editbtn&​noheader}}
 +
 +  * Definice funkce kvality
 +
 +> {{page>​..:​..:​internal:​cviceni:​spam:​krok3#​funkce_kvality&​editbtn&​noheader}}
 +
 +
 +===== TDD: BinaryConfusionMatrix =====
 +Příprava:
 +  * Projděte si specifikace třídy ''​BinaryConfusionMatrix''​.
 +  * Jaké testy (scénáře použití) dokážete pro danou třídu vymyslet?  
 +
 +> {{page>​..:​..:​internal:​cviceni:​spam:​krok3#​info&​editbtn&​noheader}}
 +
 +
 +==== Krok 0 ====
 +> {{page>​..:​..:​internal:​cviceni:​spam:​krok3#​teachers_0&​editbtn&​noheader}}
 +
 +++++ 0. Vytvoření kostry |
 +TDD nás nabádá, abychom vždy psali testy před produkčním kódem. Budeme používat modul ''​unittest''​ a testy pro třídu ''​BinaryConfusionMatrix''​ (dále jen BCF) budeme psát jako metody třídy ''​BinaryConfusionMatrixTest''​. Založme tedy soubor ''​test_confmat.py''​ a vložme do něj následující kostru:
 +
 +<code python>
 +import unittest
 +
 +class BinaryConfusionMatrixTest(unittest.TestCase):​
 +    pass
 +    ​
 +if __name__=='​__main__':​
 +    unittest.main()
 +</​code>​
 +
 +Poslední část (''​if <​nowiki>​__name__</​nowiki>​...''​) je nutná tehdy, když chcete testy spouštět v shellu nebo v debuggeru. Pokud využijete nástroj Testing z PyCharm, tato část nutná není.
 +
 +Spusťte test. Měli byste vidět, že proběhlo 0 testů.
 +
 +Doplňme teď do testu import třídy BCF z modulu ''​confmat'',​ kterou chceme testovat:
 +
 +<code python>
 +from confmat import BinaryConfusionMatrix
 +</​code>​
 +
 +Když modul s testem spustíme (viz předchozí cvičení), dostaneme ''​ImportError''​. Jak by ne, modul ''​confmat''​ neexistuje. Založme tedy soubor ''​confmat.py''​. ​
 +Po opětovném spuštění modulu s testem opět dostaneme ''​ImportError'',​ protože se snažíme importovat třídu BCM, která v modulu zatím neexistuje. Založme tedy v modulu ''​confmat''​ prázdnou třídu ''​BinaryConfusionMatrix''​. ​
 +
 +<code python>
 +class BinaryConfusionMatrix:​
 +    pass
 +</​code>​
 +
 +Test nyní nekončí chybou, bezva, máme připravenou kostru pro naše testy. V modulu ''​test_confmat''​ ovšem zatím žádné skutečné testy nemáme. Zkusme je tedy postupně vytvořit.
 +++++
 +
 +==== Krok 1 ====
 +++++ 1. Po vytvoření třídy jsou čítače vynulovány |
 +Doplňte do testové třídy následující test. Aby třída byla obecná, musíme jí při vytváření předat kódy, jimiž budeme označovat spam a korektní emaily. Následně se pokusíme získat matici ve formě slovníku a tento slovník porovnáme s očekávanými hodnotami, které by měly být hned po vytvoření objektu samozřejmě nulové.
 +<code python> ​       ​
 +    def test_countersAreZero_afterCreation(self):​
 +        # Prepare fixture
 +        cm = BinaryConfusionMatrix(pos_tag=SPAM_TAG,​ neg_tag=HAM_TAG)
 +        # Exercise the SUT
 +        cmdict = cm.as_dict()
 +        # Assert
 +        self.assertDictEqual(cmdict,​ {'​tp':​ 0, '​tn':​ 0, '​fp':​ 0, '​fn':​ 0})
 +</​code>​
 +
 +Spusťte test. Test skončí s chybou ''​NameError'',​ protože nemáme definovány konstanty ''​SPAM_TAG''​ a ''​HAM_TAG''​. Jelikož by tyto konstanty měly mít platnost co nejmenší, aby neovlivňovaly žádné další testy, definujme tyto konstanty přímo v modulu ''​test_confmat''​. Jejich hodnoty si pro účely testování můžete zvolit prakticky libovolně.
 +
 +<code python>
 +SPAM_TAG = '​SPAM'​
 +HAM_TAG = '​OK'​
 +</​code>​
 +
 +Spusťte test. Dostanete výjimku ''​TypeError'',​ protože třída BCM nemá definovanou metodu ''<​nowiki>​__init__</​nowiki>''​ se dvěma argumenty. Doplňme ji tedy (do třídy BCM). Zapamatujme si kódy pro spam a ham a inicializujme čítače TP, TN, FP, FN.
 +
 +<code python>
 +    def __init__(self,​ pos_tag, neg_tag):
 +        self.tp = 0
 +        self.tn = 0
 +        self.fp = 0
 +        self.fn = 0
 +        self.pos_tag = pos_tag
 +        self.neg_tag = neg_tag
 +</​code>​
 +
 +Spusťte test. Třída teď jde instanciovat. Dostáváme ale ''​AttributeError'',​ protože třída BCM nemá metodu ''​as_dict()''​. Doplňme ji do třídy BCM: 
 +<code python>
 +    def as_dict(self):​
 +        """​Return the conf. mat. as a dictionary."""​
 +        return {'​tp':​ self.tp, '​tn':​ self.tn, '​fp':​ self.fp, '​fn':​ self.fn}
 +</​code>​
 +
 +Spusťte test. Měl by proběhnout bez chyby. V čem je hodnota tohoto testu? Zaručí nám, že pokud se v budoucnu budeme '​hrabat'​ v metodě ''<​nowiki>​__init__</​nowiki>​()''​ nebo ''​as_dict()''​ a zaneseme tam nechtěně nějakou chybu, pak ji test s velkou pravděpodobností odhalí.
 +++++
 +
 +
 +
 +==== Krok 2 ====
 +> {{page>​..:​..:​internal:​cviceni:​spam:​krok3#​teachers_2&​editbtn&​noheader}}
 +
 +++++ 2. Metoda update() správně upraví čítač TP |
 +Vytvořme test:
 +<code python>
 +    def test_updatesTPcorrectly(self):​
 +        # Prepare fixture
 +        cm = BinaryConfusionMatrix(pos_tag=SPAM_TAG,​ neg_tag=HAM_TAG)
 +        # Exercise the SUT
 +        cm.update(SPAM_TAG,​ SPAM_TAG)
 +        # Assert
 +        self.assertDictEqual(cm.as_dict(), ​
 +                             ​{'​tp':​ 1, '​tn':​ 0, '​fp':​ 0, '​fn':​ 0})
 +</​code>​
 +
 +Spusťte test. Dostáváme ''​AttributeError'',​ protože BCM nemá metodu ''​update()''​. Vytvořme v BCM prázdnou metodu ''​update()''​ se dvěma parametry ''​truth''​ a ''​prediction''​. ​
 +Spusťte test. Metoda nyní lze zavolat, ale nefunguje tak, jak by měla (''​AssertionError''​). Ve výsledku testu i vidíme, kde se vrácený slovník liší od očekávaného. Doplňme tedy její tělo: očekáváme,​ že metoda zvýší čítač TP. Vyplňme tedy kód metody:
 +<code python>
 +    def update(self,​ truth, prediction):​
 +        """​Compare the truth with the prediction and increment the related counter."""​
 +        self.tp += 1
 +</​code>​
 +Spusťte test. Měl by projít bez chyby.
 +++++
 +
 +==== Krok 3 ====
 +
 +++++ 3. Ruční refactoring opakujícího se kódu |
 +Prohlédněte si vaše 2 testovací metody. Nemají něco společného?​
 +Mají, zdá se, že ve všech testech budeme potřebovat čerstvou instanci BCM. Extrahujme tedy její vytvoření do metody ''​setUp()''​. Abychom vytvořenou instanci BCM mohli používat i v jiných metodách, musíme ji přiřadit do členské proměnné testové třídy. Z testových metod pak odstraníme vytváření BCM a budeme rovnou používat tu instanci v členské proměnné (použití ''​self''​). Kód testové třídy by po změnách měl vypadat takto: ​         ​
 +<code python>
 +class BinaryConfusionMatrixTest(unittest.TestCase):​
 +    ​
 +    def setUp(self):​
 +        # Prepare fixture
 +        self.cm = BinaryConfusionMatrix(pos_tag=SPAM_TAG,​ neg_tag=HAM_TAG)
 +        ​
 +    def test_countersAreZero_afterCreation(self):​
 +        # Exercise the SUT
 +        cmdict = self.cm.as_dict()
 +        # Assert
 +        self.assertDictEqual(cmdict,​ {'​tp':​ 0, '​tn':​ 0, '​fp':​ 0, '​fn':​ 0})
 +        ​
 +    def test_updatesTPcorrectly(self):​
 +        # Exercise the SUT
 +        self.cm.update(SPAM_TAG,​ SPAM_TAG)
 +        # Assert
 +        self.assertDictEqual(self.cm.as_dict(), ​
 +                             ​{'​tp':​ 1, '​tn':​ 0, '​fp':​ 0, '​fn':​ 0})
 +</​code>​
 +Spusťte testy. Měly by proběhnout v pořádku. Ale možná jste při opravách někde něco přehlédli,​ testy by vám pak na onom místě měly selhat.
 +++++
 +
 +
 +==== Krok 4 ====
 +++++ 4. Metoda update() správně upraví čítač TN |
 +Vytvořme test:
 +<code python>
 +    def test_updatesTNcorrectly(self):​
 +        # Exercise the SUT
 +        self.cm.update(HAM_TAG,​ HAM_TAG)
 +        # Assert
 +        self.assertDictEqual(self.cm.as_dict(), ​
 +                             ​{'​tp':​ 0, '​tn':​ 1, '​fp':​ 0, '​fn':​ 0})
 +</​code>​
 +
 +Spusťte test. Metoda selhává (''​AssertionError''​),​ protože místo čítače TN inkrementovala čítač TP. Upravme tedy její tělo co nejjednodušším způsobem tak, aby procházel test:
 +<code python>
 +    def update(self,​ truth, prediction):​
 +        """​Compare the truth with the prediction and increment the related counter."""​
 +        if prediction == self.pos_tag:​
 +            self.tp += 1
 +        elif prediction == self.neg_tag:​
 +            self.tn += 1
 +</​code>​
 +Spusťte test. Měl by projít bez chyby.
 +++++
 +
 +==== Krok 5 ====
 +++++ 5. Metoda update() správně upraví čítač FP |
 +Vytvořme test:
 +<code python>
 +    def test_updatesFPcorrectly(self):​
 +        # Exercise the SUT
 +        self.cm.update(HAM_TAG,​ SPAM_TAG)
 +        # Assert
 +        self.assertDictEqual(self.cm.as_dict(), ​
 +                             ​{'​tp':​ 0, '​tn':​ 0, '​fp':​ 1, '​fn':​ 0})
 +</​code>​
 +
 +Spusťte test. Metoda selhává (''​AssertionError''​). Upravme tedy její tělo co nejjednodušším způsobem tak, aby procházel test:
 +<code python>
 +    def update(self,​ truth, prediction):​
 +        """​Compare the truth with the prediction and increment the related counter."""​
 +        if prediction == self.pos_tag:​
 +            if truth == prediction:
 +                self.tp += 1
 +            else:
 +                self.fp += 1
 +        elif prediction == self.neg_tag:​
 +            self.tn += 1
 +</​code>​
 +Spusťte test. Měl by projít bez chyby.
 +++++
 +
 +==== Krok 6 ====
 +++++ 6. Metoda update() správně upraví čítače FN |
 +Vytvořme test:
 +<code python>
 +    def test_updatesFNcorrectly(self):​
 +        # Exercise the SUT
 +        self.cm.update(SPAM_TAG,​ HAM_TAG)
 +        # Assert
 +        self.assertDictEqual(self.cm.as_dict(), ​
 +                             ​{'​tp':​ 0, '​tn':​ 0, '​fp':​ 0, '​fn':​ 1})
 +</​code>​
 +
 +Spusťte test. Metoda selhává (''​AssertionError''​). Upravme tedy její tělo co nejjednodušším způsobem tak, aby procházel test:
 +<code python>
 +    def update(self,​ truth, prediction):​
 +        """​Compare the truth with the prediction and increment the related counter."""​
 +        if prediction == self.pos_tag:​
 +            if truth == prediction:
 +                self.tp += 1
 +            else:
 +                self.fp += 1
 +        elif prediction == self.neg_tag:​
 +            if truth == prediction:
 +                self.tn += 1
 +            else:
 +                self.fn += 1
 +</​code>​
 +Spusťte test. Měl by projít bez chyby.
 +++++
 +
 +
 +==== Krok 7 ====
 +> {{page>​..:​..:​internal:​cviceni:​spam:​krok3#​teachers_7&​editbtn&​noheader}}
 +
 +++++ 7. Co má metoda update dělat při chybném vstupu? |
 +Metoda ''​update(truth,​ prediction)''​ přebírá 2 argumenty, jejichž hodnoty by vždy měly být rovny pozitivnímu štítku nebo negativnímu štítku, které jsme zadali při vytváření instannce BCM. Co má metoda dělat, pokud některý z těchto argumentů tuto podmínku nesplňuje?
 +
 +Metoda by v takovém případě nejspíš měla vyhodit výjimku ''​ValueError''​. Jak se dá toto otestovat?
 +
 +Pokud v tuto chvíli nevíte nic o výjimkách,​ zkuste si o nich [[http://​openbookproject.net/​thinkcs/​python/​english3e/​exceptions.html|něco přečíst]],​ diskutujte se cvičícím,​ ptejte se na fóru. Nevadí, pokud tomuto tématu nerozumíte,​ ale bude dobře, když o něm budete alespoň vědět. ​
 +
 +<code python>
 +    def test_update_raisesValueError_forWrongTruthValue(self):​
 +        # Assert and exercise the SUT
 +        with self.assertRaises(ValueError):​
 +            self.cm.update('​a bad value',​ SPAM_TAG)
 +        ​
 +    def test_update_raisesValueError_forWrongPredictionValue(self):​
 +        # Assert and exercise the SUT
 +        with self.assertRaises(ValueError):​
 +            self.cm.update(SPAM_TAG,​ 'a bad value'​)
 +</​code>​
 +
 +Spusťte test. Měl by selhat. Upravme metodu ''​update()''​ třeba takto:
 +
 +<code python>
 +    def update(self,​ truth, prediction):​
 +        """​Compare the truth with the prediction and increment the related counter."""​
 +        if truth not in (self.pos_tag,​ self.neg_tag):​
 +            raise ValueError('​The "​truth"​ parameter can be either %s or %s.' \
 +                             % (self.pos_tag,​ self.neg_tag))
 +        if prediction not in (self.pos_tag,​ self.neg_tag):​
 +            raise ValueError('​The "​prediction"​ parameter can be either %s or %s.' \
 +                             % (self.pos_tag,​ self.neg_tag))
 +        if prediction == self.pos_tag:​
 +</​code>​
 +
 +Spusťte test. Měl by proběhnout bez chyb.
 +
 +Hmm, ale opakuje se tam kód na kontrolu hodnot argumentů. Proveďte refactoring.
 +++++
 +
 +==== Krok 8 ====
 +> {{page>​..:​..:​internal:​cviceni:​spam:​krok3#​teachers_8&​editbtn&​noheader}}
 +
 +++++ 8. Refactoring s pomocí IDE |
 +Metoda ''​update()''​ teď má na začátku dva téměř stejné bloky kódu: jeden kontroluje hodnotu parametru ''​truth'',​ druhý hodnotu parametru ''​prediction''​. Udělejme pro kontrolu hodnoty zvláštní metodu, kterou jen pak 2x zavoláme.
 +
 +Označte následující blok kódu:
 +<code python>
 +        if truth not in (self.pos_tag,​ self.neg_tag):​
 +            raise ValueError('​The "​truth"​ parameter can be either %s or %s.' \
 +                             % (self.pos_tag,​ self.neg_tag))
 +</​code>​
 +
 +Z menu **Refactor** nebo z nabídky **Refactor** v kontextovém menu vyberte položku **Extract** a následně **Method**. V dialogu **Extract method** zvolte jako jméno metody třeba ''​check_value_of''​ a klikněte na tlačítko **OK**.
 +
 +Ve vašem kódu by měly nastat 2 změny:
 +  * Automaticky byla vytvořena metoda ''​check_value_of()''​ s následujícím tělem:<​code python> ​   def check_value_of(self,​ truth):
 +        if truth not in (self.pos_tag,​ self.neg_tag):​
 +            raise ValueError('​The "​truth"​ parameter can be either %s or %s.' \
 +                             % (self.pos_tag,​ self.neg_tag))</​code>​
 +  * Na místě, kde se tento kód vykonával, je teď jen volání nové metody.
 +
 +Spusťte testy, abyste si ověřili, že IDE nezměnilo funkčnost kódu. Nyní bychom ale měli trochu upravit tělo nové metody a také jejím voláním nahradit druhý opakující se blok kódu. To už musíme udělat ručně. Upravme tedy ''​check_value_of()''​ třeba takto:
 +<code python>
 +    def check_value_of(self,​ value):
 +        """​Raise ValueError if var does not contain either positive or negative tag."""​
 +        if value not in (self.pos_tag,​ self.neg_tag):​
 +            raise ValueError('​The arguments may be either %s or %s.' \
 +                             % (self.pos_tag,​ self.neg_tag))
 +</​code>​
 +
 +A začátek metody ''​update()''​ upravte takto:
 +<code python>
 +    def update(self,​ truth, prediction):​
 +        """​Compare the truth with the prediction and increment the related counter."""​
 +        self.check_value_of(truth)
 +        self.check_value_of(prediction)
 +        if prediction == self.pos_tag:​
 +            ...
 +</​code> ​     ​
 +Spusťte testy pro kontrolu, zda jsme do kódu nezanesli nějakou chybu.
 +++++
 +
 +
 +==== Krok 9 ====
 +++++ 9. Metoda compute_from_dicts() správně updatuje čítače |
 +Vytvořme test: 
 +<code python>
 +    def test_computeFromDicts_allCasesOnce(self):​
 +        # Prepare fixture
 +        truth = {1: SPAM_TAG,
 +                 2: SPAM_TAG,
 +                 3: HAM_TAG,
 +                 4: HAM_TAG}
 +        prediction = {1: SPAM_TAG,
 +                      2: HAM_TAG,
 +                      3: SPAM_TAG,
 +                      4: HAM_TAG}
 +        # Excercise the SUT
 +        self.cm.compute_from_dicts(truth,​ prediction)
 +        # Assert
 +        self.assertDictEqual(self.cm.as_dict(), ​
 +                             ​{'​tp':​ 1, '​tn':​ 1, '​fp':​ 1, '​fn':​ 1})
 +</​code>​
 +
 +Spusťme test. Selhává (''​AttributeError''​),​ protože třída BCM nemá metodu ''​compute_from_dicts()''​. Vytvořme ji.
 +
 +<code python>
 +    def compute_from_dicts(self,​ truth_dict, pred_dict):
 +        """​Update the matrix using the corresponding values from both dictionaries.
 +        It is assumed that both dicts have the same keys.
 +        """​
 +        for key in truth_dict:
 +            self.update(truth_dict[key],​ pred_dict[key])
 +</​code>​
 +
 +Spusťe test. Měl by proběhnout v pořádku.
 +++++
 +
 +
 +==== Krok 10 ====
 +++++ 10. Diskuse: další testy? |
 +  * Jak bychom dál měli otestovat metodu compute_from_dicts()?​
 +    * Co když slovníky nebudou mít shodné klíče? V naší aplikaci by se to stát nemělo, bude-li dobře napsaná. Ale co když uděláme někde chybu a tato situace nastane?
 +    * Měli bychom to v tichosti přejít a prostě napočítat matici záměn jen z hodnot se stejnými klíči? Nebo vyhodit výjimku?
 +++++  ​
 +
 +
 +
 +
 +===== Hádanka =====
 +> {{page>​courses:​b4b33rph:​internal:​puzzles#​cviceni_9}}
 +
 +===== Programovací trik =====
 +++++ Volání funkce s argumenty připravenými v seznamu nebo slovníku |
 +Jak volat metodu či funkci, pro jejíž argumenty máme hodnoty připravené v seznamu nebo slovníku? ​
 +
 +Mějme následující funkci:
 +<code python>
 +def print_args(a,​ b):
 +    print(a, b)
 +</​code>​
 +
 +Předpokládejme,​ že bychom pro tuto funkci měli hodnoty parametrů ''​a''​ a ''​b''​ připraveny v poli nebo slovníku:
 +
 +<code python>
 +>>>​ arg_list = ['​Seznam',​ '​hodnot'​]
 +>>>​ arg_dict = {'​a':​ '​Slovnik',​ '​b':​ '​hodnot'​}
 +</​code>​
 +
 +Kdybychom chtěli funkci spustit s těmito hodnotami, museli bychom seznam i slovník ručně rozebrat na prvky a ty pak předat funkci, např. takto:
 +
 +<code python>
 +>>>​ print_args(arg_list[0],​ arg_list[1])
 +Seznam hodnot
 +>>>​ print_args(arg_dict['​a'​],​ arg_dict['​b'​])
 +Slovnik hodnot
 +</​code>​
 +
 +Takový způsob volání funkce je ale nepohodlný,​ obzvlášť tehdy, bude-li mít seznam či slovník mnohem více prvků. Python proto nabízí **trik**:
 +
 +<code python>
 +>>>​ print_args(*arg_list)
 +Seznam hodnot
 +>>>​ print_args(**arg_dict)
 +Slovnik hodnot
 +</​code>​
 +
 +Tento trik můžete s výhodou použít, až budete hodnoty prvků matice záměn získané metodou ''​as_dict()''​ přeávat do funkce ''​quality_score()''​.
 +
 +++++
 +
 +===== Diskuse: jednoduché filtry =====
 +Pokud jste tak ještě neučinili, přečtěte si specifikace [[..:​spam:​krok4|kroku 4]] úlohy Spam filtr:
 +  * Jaké metody filtr musí mít?
 +  * Musejí být implementovány všechny?
 +  * Jak tedy bude implementace jednoduchých filtrů přibližně vypadat?
 +
 +
 +===== Kontrola DÚ z minulého cvičení =====
 +  * Zkontrolovat,​ zda funkce ''​read_classification_from_file()''​ a třída ''​Corpus''​ procházejí unit testem.
 +  * Pokud ne, probrat výstup z unit testu a nasměrovat na správnou cestu!
 +
 +===== Programovací úloha =====
 +  * Dokončete [[..:​spam:​krok3|krok 3]] úlohy spam filtr, tedy funkci pro výpočet kvality filtru. Od neděle za týden budete modul ''​quality''​ společně s dalšími potřebnými moduly odevzdávat do upload systému!
 +  * Můžete začít pracovat na [[..:​spam:​krok4|kroku 4]] úlohy spam filtr, tj. implementujte 3 jednoduché filtry.
 +
 +
 +====== Domácí úkol ======
 +
 +===== Programování =====
 +  * Vyzkoušejte si/​proveďte/​dokončete programování třídy BinaryConfusionMatrix podle výše uvedeného návodu. Snažte se přitom pochopit nejen co děláte, ale i proč to děláte.
 +  * Pokud jste nestihli na cvičení, dokončete [[..:​spam:​krok3|krok 3]] úlohy spam filtr, tedy funkci pro výpočet kvality filtru. Tuto funkci budete za týden odevzdávat do upload systému a bude se hodnotit!!!
 +  * Dokončete [[..:​spam:​krok4|krok 4]] úlohy spam filtr, implementujte 3 jednoduché filtry:
 +    * Zkuste je aplikovat na nějaký korpus.
 +    * Použijte funkci z kroku 3 na odhad kvality filtru.
 +    * Jaké hodnoty kvality jste dostali pro naše 3 jednoduché filtry???
 +
 +
 +===== Příprava =====
 +Přečtete si něco o dědičnosti:​
 +> {{section>​..:​spam:​krok4#​priprava:​dedicnost&​noheader&​editbtn}}
 +
 +Příprava na krok 5 úlohy spam filtr:
 +> {{section>​..:​spam:​krok5#​priprava&​noheader&​editbtn}}
 +
  
courses/b4b33rph/cviceni/program_po_tydnech/tyden_09.txt · Last modified: 2018/12/04 16:25 by xposik