TDD w Pythonie

Test Driven Development (TDD) jest metodą tworzenia oprogramowania, która testowanie aplikacji traktuje jako główny czynnik napędzający rozwój. Inaczej niż w metodach klasycznych, programista najpierw zajmuje się pisaniem testu sprawdzającego działanie danej funkcjonalności, a dopiero potem tą funkcjonalność koduje. Rozwiązanie to ma szereg zalet. Pisanie testu pozwala nam dokładnie sprecyzować czego oczekujemy od aplikacji. Automatyczny test jest niezastąpionym pomocnikiem w fazie implementacji, gdyż na bieżąco śledzić możemy własne postępy. Przydaje się również później, gdy dodając nową funkcjonalność chcemy być pewni, że przy okazji niczego nie psujemy. Oczywiście by metoda była skuteczna, pisanie testów powinno być możliwie szybkie i proste. By zaspokoić tę potrzebę powstało wiele różnych bibliotek i środowisk testowych, pozwalających łatwo tworzyć i uruchamiać duże zestawy testów. Są dostępne praktycznie dla każdego języka programowania i potrafią różnić się tak bardzo, jak same języki. W artykule tym przedstawię proces rozwoju prostego modułu w Pythonie. Na bibliotekę testową wybrałem doctest, jako, że jest ona według mnie najbardziej pythonowa ze wszystkich dostępnych. Charakteryzuje się szczerą prostotą i łatwością użycia, tym samym podążając za duchem języka.

Naszym zadaniem będzie napisanie klasy, która będzie się zachowywać jak posortowany zbiór słów. Obiekty tej klasy jako tekst reprezentowane będą przez słowa rozdzielone pojedynczą spacją. Będzie można ich używać jako implementacji listy tzw. tagów, jakie spotkać można na serwisach takich jak delicious czy Technorati. Używając notacji interpretera Pythona, przykład użycia takiej klasy można zapisać następująco:

>>> T = TagList(['python', 'ruby'])
>>> T
'python ruby'
>>> T = TagList('ruby python ruby')
>>> T
'python ruby'
>>> T.append('lisp')
>>> T
'lisp python ruby'

Jak widać, inicjować będzie można tę klasę zarówno za pomocą listy słów, jak i za pomocą pojedynczego stringa zawierającego słowa rozdzielone spacjami. Znamy już podstawowe wymagania. Nasza klasa musi:

  • automatycznie się sortować
  • automatycznie usuwać zduplikowane elementy
  • być reprezentowana jako ciąg słów rozdzielonych spacjami

Jako, że każde z tych zadań stanowi wyzwanie samo w sobie, nasz główny cel osiągniemy implementując wymienione podzadania po kolei, jedno po drugim. Najpierw dodamy tekstową reprezentację, następnie pozbywając się duplikatów uczynimy listę zbiorem, na końcu implementując automatyczne sortowanie. Do dzieła.

Uwaga: W tekście pojawiać się będą tylko fragmenty kodu wynikające z aktualnego kontekstu. Wycięte części oznaczone są wielokropkiem. Na końcu artykułu dostępny jest ostateczny kod źródłowy całego modułu.

Pisanie i uruchamianie testów

Zgodnie z opisem TDD najpierw formułujemy swoje wymagania za pomocą testu. Najciekawsze w bibliotece doctest jest to, że testy piszemy za pomocą wyimaginowanych sesji interpretera. Formułujemy więc własne oczekiwania przy pomocy wyrażeń i wartości, jakie chcemy by przyjmowały. Przykładowa sesja interpretera, którą zacytowałem powyżej, jest więc w rzeczywistości naszym pierwszym testem! Testy umieszczamy w tzw. docstringach, czyli stringach umieszczanych zaraz po pierwszej linii definicji funkcji lub klasy, służących jako dokumentacja. Biblioteka doctest sama zajmie się rozpoznaniem i interpretacją tych testów. Definicja naszej klasy wygląda więc następująco:

class TagList(list):
    """Lista unikalnych słów (tagów).

    Listę tagów możemy inicjować za pomocą listy lub stringa.
    Wynik jest zawsze posortowany.
        >>> T = TagList(['python', 'ruby'])
        >>> T
        'python ruby'
        >>> T = TagList('python ruby python')
        >>> T
        'python ruby'
        >>> T.append('lisp')
        >>> T
        'lisp python ruby'
    "
""
    pass

Jak widać z przykładu nie musimy się martwić o poziomy wcięcia, czy o rozróżnienie naszych komentarzy od samych testów. Biblioteka doctest bez problemu sobie z tym radzi. Pisanie testów to jedno, ale chcielibyśmy je wreszcie odpalić. Jako że testujemy oddzielny moduł, a nie aplikację, wystarczy dodać na koniec pliku poniższe cztery linijki kodu. Importują one bibliotekę doctest i wskazują jej do przetestowania kod aktualnego modułu.

if __name__ == '__main__':
    import doctest
    doctest.testmod()

Zapisujemy nasz plik jako taglist.py i odpalamy:

$ python taglist.py

*****************************************************************
Failure in example: T
from line #5 of __main__.TagList
Expected: 'python ruby'
Got: ['python', 'ruby']
*****************************************************************
Failure in example: T
from line #8 of __main__.TagList
Expected: 'python ruby'
Got:
['p', 'y', 't', 'h', 'o', 'n', ' ', 'r', 'u', 'b', 'y', ' ', 'p',
'y', 't', 'h', 'o', 'n']
*****************************************************************
Failure in example: T
from line #11 of __main__.TagList
Expected: 'lisp python ruby'
Got:
['p', 'y', 't', 'h', 'o', 'n', ' ', 'r', 'u', 'b', 'y', ' ', 'p',
'y', 't', 'h', 'o', 'n', 'lisp']
*****************************************************************
1 items had failures:
   3 of   6 in __main__.TagList
***Test Failed*** 3 failures.

Spośród wszystkich linii interesują nas najbardziej te informujące o tym dlaczego dany test się nie powiódł. Są to linie Expected: i Got:, które mówią o tym czego się spodziewano (ang. expected), a co zamiast tego otrzymano (ang. got). W pierszym teście na przykład spodziewaliśmy się reprezentacji 'python ruby', a otrzymaliśmy zamiast tego ['python', 'ruby'] (czyli standardową reprezentację list w Pythonie). Cóż, spróbujmy poprawić wynik pierwszego testu, dopisując własną metodę zwracającą reprezentację obiektu. Jej symboliczna nazwa to __repr__.

class TagList(list):
    # ...
    def __repr__(self):
        return ' '.join(self)

Łączymy wszystkie elementy listy za pomocą spacji. Wynik testu niestety jest wciąż niezadowalający (wyciąłem wyniki pozostałych testów, jako że są w tej chwili bez znaczenia):

Failure in example: T
from line #5 of __main__.TagList
Expected: 'python ruby'
Got: python ruby

Brakuje cudzysłowów! Jeżeli chcemy, by lista zachowywała się rzeczywiście jak ciąg znaków, musimy również zdefiniować dla niej metodę __str__. Konwersję do ciągu znaków wykonywać więc będziemy właśnie za pomocą metody __str__, reprezentację obiektu uzyskując poprzez zwróconego stringa.

class TagList(list):
    # ...
    def __repr__(self):
        return repr(self.__str__())

    def __str__(self):
        return ' '.join(self)

Po tych zmianach nasz kod przechodzi pierwszy test:

*****************************************************************
Failure in example: T
from line #8 of __main__.TagList
Expected: 'python ruby'
Got: 'p y t h o n   r u b y   p y t h o n'
*****************************************************************
Failure in example: T
from line #11 of __main__.TagList
Expected: 'lisp python ruby'
Got: 'p y t h o n   r u b y   p y t h o n lisp'
*****************************************************************
1 items had failures:
   2 of   6 in __main__.TagList
***Test Failed*** 2 failures.

Bierzemy się za następny. Widać w nim, że pojedyncze znaki są traktowane jako elementy listy. Wynika to z tego, że inicjalizator listy w Pythonie nie traktuje podanego stringa jako całości, ale jako listę znaków. Musimy więc samemu napisać funkcję inicjującą. Jej symboliczna nazwa to __init__.

class TagList(list):
    # ...
    def __init__(self, tags=None):
        if isinstance(tags, basestring):
            self.extend(tags.split())
        elif isinstance(tags, list):
            self.extend(tags)

Po uruchomieniu testu widać, że reprezentacja już jest prawidłowa, jednak brakuje nam dwóch istotnych elementów: sortowania i usuwania powtarzających się elementów.

*****************************************************************
Failure in example: T
from line #8 of __main__.TagList
Expected: 'python ruby'
Got: 'python ruby python'
*****************************************************************
Failure in example: T
from line #11 of __main__.TagList
Expected: 'lisp python ruby'
Got: 'python ruby python lisp'

Pisanie kodu

Jako, że drugi test sprawdza, czy lista nie zawiera duplikatów, zaimplementujemy właśnie pozbywanie się powtarzających się elementów. Zaczniemy od zdefiniowania własnej metody extend, która przy dodawaniu pominie elementy już w liście zawarte.

class TagList(list):
    # ...
    def extend(self, iterable):
        for element in iterable:
            if element not in self:
                self.append(element)

Jak widać, dodajemy tylko elementy, których na liście jeszcze nie ma. Uruchomienie testu potwierdza poprawność właśnie napisanego kodu - drugi test kończy się powodzeniem. Ostatnią rzeczą, jaka nam została jest uczynienie listy automatycznie sortującą się. Przy okazji mamy obsłużyć metodę append. Najprostszym sposobem jest wywoływanie self.sort() po każdej modyfikacji. I ten właśnie sposób wykorzystamy przy pisaniu metody append.

class TagList(list):
    # ...
    def append(self, object):
        if object not in self:
            list.append(self, object)
        self.sort()

Jeżeli uruchomimy teraz test, przekonamy się, że moduł doctest nie wykryje żadnych błędów. Nasz kod dla podanych przykładów działa poprawnie. To oczywiście nie znaczy, że jest gotowy. Patrząc na metody extend i append można zauważyć powtarzający się warunek sprawdzający czy dodawanego elementu nie ma już na liście. Jako, że samo extend korzysta z metody append, warunek może zostać w niej pominięty. extend skraca się więc do następującej postaci:

class TagList(list):
    # ...
    def extend(self, iterable):
        for element in iterable:
            self.append(element)

Jak po każdej zmianie, tak i po tej uruchamiamy test, by przekonać się, czy nadal wszystko działa jak należy. Szczęśliwie okazuje się to prawdą i możemy przejść dalej.

Cykl rozwoju kodu

Opisany powyżej proces rozwoju klasy TagList jest wzorcowym przykładem stosowanego TDD. Najpierw napisaliśmy zestaw testów, które obejmowały wymaganą funkcjonalność, następnie napisaliśmy kod przechodzący testy z wynikiem pozytywnym, by wreszcie na końcu skrócić i uogólnić nasz kod, cały czas pamiętając o sprawdzaniu jego poprawności. Cykl ten można zwięźle ująć w trzech krokach:

Te trzy kroki powtarza się tak długo, aż uwzględnimy w naszych testach całą wymaganą funkcjonalność, a zatem i tak długo, aż nasz kod poprawnie ją zaimplementuje. Dynamizm tego procesu pozwala bardzo łatwo i szybko reagować na zmiany w wymaganiach projektu. Do tego mamy pewność, że każdy napisany kawałek kodu jest sprawdzany przez odpowiadający mu test. Specyfika biblioteki testującej doctest czyni zaś testy świetną dokumentacją. Chociaż nie każdy element da się testować w ten sposób, dla większości zastosowań jest to rozwiązanie naprawdę dobre. Wystarczy jednak pochwał dla TDD, zajmijmy się dalej rozwojem klasy TagList.

Pierwsze, co rzuca się w oczy, to fakt, że modyfikować tablicę można nie tylko przy pomocy metod extend i append. Często spotyka się wyrażenia wykorzystujące operator dodawania, na przykładzie czego napiszemy czwarty test.

class TagList(list):
    """
        ...
        >>> T += ['lisp', 'haskell']
        >>> T
        'haskell lisp python ruby'
    "
""

Skrócony operator dodawania z przypisaniem obsługuje magiczna metoda __iadd__. Dokumentacja mówi nam:

x.__iadd__(y) <==> x+=y

Oznacza to, że zapis x += y spowoduje wywołanie metody __iadd__ na obiekcie x z argumentem y. Działanie tego operatora dla list jest identyczne jak wywołanie metody extend, z tą różnicą, że funkcja __iadd__ musi zwrócić wartość. Implementacja wygląda więc następująco:

class TagList(list):
    # ...
    def __iadd__(self, iterable):
        self.extend(iterable)
        return self

Uruchomienie testów nie zgłasza błędów, możemy więc przejść do implementacji kolejnych funkcji. Żeby uczynić klasę kompletną dodamy obsługę wszystkich pozostałych metod modyfikujących listę. Tym razem podejdziemy jednak bardziej restrykcyjnie niż do append. Metoda insert wstawia podany element w określone miejsce listy. Takie działanie nie ma sensu w przypadku samo sortującej się listy. Na tej samej zasadzie nie ma sensu operacja odwrócenia kolejności elementów takiej listy (metoda reverse). Zabronimy również mnożenia listy przez liczbę (powielania jej elementów), ustawiania konkretnych elementów (lista[3] = wartość), jak i jej fragmentów (lista[1:3] = [17,19]). Za te operacje odpowiadają magiczne metody __imul__, __setitem__ i __setslice__. Najlepszą rzeczą, jaką może zrobić obiekt przy wywołaniu jednej z niedozwolonych metod, jest rzucenie wyjątkiem. W tym przypadku będzie to wyjątek TypeError. Poniżej zapisałem testy dla każdej z metod.

class TagList(list):
    """
        ...

    W przypadku wywołania niedozwolnej metody, obiekt rzuci
    wyjątkiem TypeError.
        >>> T = TagList()
        >>> T.insert(3, "
wartość")
        Traceback (most recent call last):
          ...
        TypeError: Nie można użyć metody insert na obiekcie typu TagList.
        >>> T.reverse()
        Traceback (most recent call last):
          ...
        TypeError: Nie można użyć metody reverse na obiekcie typu TagList.
        >>> T *= 3
        Traceback (most recent call last):
          ...
        TypeError: Nie można użyć metody __imul__ na obiekcie typu TagList.
        >>> T[0] = "
wartość"
        Traceback (most recent call last):
          ...
        TypeError: Nie można użyć metody __setitem__ na obiekcie typu TagList.
        >>> T[0:2] = [17, 19]
        Traceback (most recent call last):
          ...
        TypeError: Nie można użyć metody __setslice__ na obiekcie typu TagList.
    "
""

Zanim przejdziemy do pisania metod przyjrzyjmy się jeszcze specjalnej składni, jaką stosuje się przy opisywaniu wyjątków dla modułu doctest. Najpierw występuje stała linijka Traceback (most recent call last):, charakteryzująca wszystkie wyjątki1. Dalej interpreter Pythona zwyczajowo umieszcza ścieżkę wywołań (ang. stack trace), po której dopiero dowiedzieć można się o typie i komunikacie wyjątku. Jako, że kod może zostać wywołany na różne sposoby i z różnych miejsc, dobrze jest po prostu pomijać ścieżkę wywołań i zastępować ją wielokropkiem. doctest weźmie wtedy pod uwagę tylko typ i komunikat błędu.

Każda z omawianych metod powinna działać w ten sam sposób, ma rzucić wyjątkiem TypError o określonym komunikacie błędu. Jako, że komunikat różni się tylko jednym słowem uogólnimy wszystkie metody prostym domknięciem (ang. closure):

class TagList(list):
    # ...
    def _bad_method(name):
        def method(self, *args):
            raise TypeError(
                "Nie można użyć metody %s na obiekcie typu TagList." % name)
        return method

    insert = _bad_method("insert")
    reverse = _bad_method("reverse")
    __imul__ = _bad_method("__imul__")
    __setitem__ = _bad_method("__setitem__")
    __setslice__= _bad_method("__setslice__")

Funkcja _bad_method zwraca funkcję, która rzuca wyjątkiem o komunikacie parametryzowanym argumentem name. W ten sposób osiągnęliśmy nasz cel: wyniki działania wszystkich metod są identyczne z wyjątkiem jednego słowa w komunikacie błędu. Uruchomienie testu na tak uzupełnionym kodzie potwierdza nas o jego poprawności. Fazę refaktoryzacji możemy tym razem pominąć, jako że kod wygląda całkiem przyzwoicie i nie ma za bardzo co poprawiać.

Ostateczny szlif

Chociaż osiągnęliśmy podstawową funkcjonalność jest jeszcze kilka szczegółów, które wymagają naszej uwagi. Na początku tego tekstu zdefiniowaliśmy klasę TagList jako zbiór słów. Logicznym jest więc, że elementami listy tagów nie powinny być liczby, czy też inne obiekty nie będące napisami. Tymczasem nie wprowadziliśmy jeszcze żadnego sprawdzania typu argumentów przekazywanych do obiektów typu TagList. Zaraz ten problem naprawimy.

class TagList(list):
    """
        ...

    Elementy listy mogą być wyłącznie ciągami znaków.
        >>> T = TagList([1, 2])
        Traceback (most recent call last):
          ...
        TypeError: Elementy listy muszą być stringami.
        >>> T = TagList()
        >>> T.append('python')
        >>> T.append(u'lisp')
        >>> T
        u'lisp python'
        >>> T.append(17)
        Traceback (most recent call last):
          ...
        TypeError: Elementy listy muszą być stringami.
        >>> T += [42]
        Traceback (most recent call last):
          ...
        TypeError: Elementy listy muszą być stringami.
    "
""

Powyższy test sprawdza dwie rzeczy. Po pierwsze upewnia się, że jeżeli na liście tagów znajdzie się choć jeden o typie unicode, to reprezentacja obiektu również będzie w unikodzie. Ten test tak naprawdę już ma pokrycie w kodzie, tak domyślnie działa łączenie stringów w Pythonie. Zapisałem go jednak, bo może on uchronić nas przed przyszłymi zmianami, które pożądane zachowanie mogłyby odwrócić. Pozostałe testy sprawdzają czy obiekty poprawnie wykrywają próbę przekazania im elementów innych niż stringi, zarówno na etapie inicjalizacji, jak i w momencie wywołania metody. W związku z tym, że modyfikację listy typu TagList dokonujemy wyłącznie za pomocą metody append, do implementacji sprawdzania typów wystarczy dodać w odpowiednim miejscu jeden warunek.

class TagList(list):
    # ...
    def append(self, object):
        if object not in self:
            if not isinstance(object, basestring):
                raise TypeError("Elementy listy muszą być stringami.")
            list.append(self, object)
        self.sort()

Ostatnie już dwa szczegóły dotyczą pobierania kawałków (ang. slices) listy i dodawania do siebie dwóch list, z których co najmniej jedna jest typu TagList. W obu tych przypadkach wynik ma być również typu TagList. Napisanie testów dla tych przypadków jest bardzo proste.

class TagList(list):
    """
        ...
    Pobieranie wycinka TagListy i dodawanie jej do innej listy
    powinno zwracać nową TagListę.
        >>> T = TagList('python lisp')
        >>> N = T + ['ruby', 'haskell']
        >>> N
        'haskell lisp python ruby'
        >>> isinstance(N, TagList)
        True
        >>> M = N[1:3]
        >>> M
        'lisp python'
        >>> isinstance(M, TagList)
        True
    "
""

Implementacja dodawania korzysta z magicznej metody __add__, zaś tworzenie wycinków z metody __getslice__. Sam kod zaś jest naprawdę trywialny.

class TagList(list):
    # ...
    def __add__(self, iterable):
        return TagList(list.__add__(self, iterable))

    def __getslice__(self, i, j):
        return TagList(list.__getslice__(self, i, j))

Jeżeli zapuścimy test, przekonamy się, że powyższe dwie proste definicje rzeczywiście załatwiają sprawę. Zanim jednak zakończymy ten krok, warto wprowadzić drobną korektę do kodu i zastąpić wywołanie konstruktora TagList poprzez wywołanie za pomocą self.__class__. Jeżeli tego nie zrobimy, mogą pojawić się problemy, gdy kiedyś utworzymy nową klasę dziedziczącą po TagList.

class TagList(list):
    # ...
    def __add__(self, iterable):
        return self.__class__(list.__add__(self, iterable))

    def __getslice__(self, i, j):
        return self.__class__(list.__getslice__(self, i, j))

I to już wszystko, klasa TagList jest gotowa. Oprócz działającego kodu zawiera dokumentację i kompletny zestaw testów. Mieści się w 127 linijkach, z czego ponad połowę stanowi jej docstring. Jeżeli w przyszłości będziesz miał zamiar ją modyfikować, bez strachu będziesz mógł rozszerzać jej definicję, automatyczne testy czynią testowanie prawdziwą przyjemnością. Chociaż może się wydawać, że cały proces powoduje wydłużenie czasu pisania kodu, rzeczywistość jest odmienna. Oszczędzasz sobie bowiem czasu jaki spędziłbyś nad debugowaniem niedokładnie przetestowanego i nieudokumentowanego kodu. W bliskiej perspektywie TDD zdaje się być nieopłacalne, im dłużej jednak zamierzasz rozwijać swój projekt, tym więcej korzyści odniesiesz z jego stosowania. I to tyczy się nie tylko Pythona i doctestu, ale praktycznie każdego środowiska programistycznego, na jakie możesz natrafić. Opisany przeze mnie tandem jest przykładem elegancji i użyteczności, dlatego polecam go każdemu programiście. Utopia, jaką może zdawać się przetestowany i udokumentowany kod, staje się faktem, jeżeli tylko weźmie się do serca założenia TDD. Gorąco zachęcam do zainteresowania się tematem nowoczesnych technik rozwoju oprogramowania. Odnośniki do kilku reprezentatywnych materiałów poniżej.

Odnośniki

Ostateczny kod źrodłowy klasy TagList

Kod umieszczam w domenie publicznej. Można go również pobrać w oddzielnym pliku.

#!/usr/bin/python
# -*- coding: utf-8 -*-

class TagList(list):
    """Lista unikalnych słów (tagów).

    Listę tagów możemy inicjować za pomocą listy lub stringa.
    Wynik jest zawsze posortowany.
        >>> T = TagList(['python', 'ruby'])
        >>> T
        'python ruby'
        >>> T = TagList('python ruby python')
        >>> T
        'python ruby'
        >>> T.append('lisp')
        >>> T
        'lisp python ruby'
        >>> T += ['lisp', 'haskell']
        >>> T
        'haskell lisp python ruby'

    Elementy listy mogą być wyłącznie ciągami znaków.
        >>> T = TagList([1, 2])
        Traceback (most recent call last):
          ...
        TypeError: Elementy listy muszą być stringami.
        >>> T = TagList()
        >>> T.append('python')
        >>> T.append(u'lisp')
        >>> T
        u'lisp python'
        >>> T.append(17)
        Traceback (most recent call last):
          ...
        TypeError: Elementy listy muszą być stringami.
        >>> T += [42]
        Traceback (most recent call last):
          ...
        TypeError: Elementy listy muszą być stringami.

    Pobieranie wycinka TagListy i dodawanie jej do innej listy
    powinno zwracać nową TagListę.
        >>> T = TagList('python lisp')
        >>> N = T + ['ruby', 'haskell']
        >>> N
        'haskell lisp python ruby'
        >>> isinstance(N, TagList)
        True
        >>> M = N[1:3]
        >>> M
        'lisp python'
        >>> isinstance(M, TagList)
        True

    W przypadku wywołania niedozwolnej metody, obiekt rzuci
    wyjątkiem TypeError.
        >>> T = TagList()
        >>> T.insert(3, "
wartość")
        Traceback (most recent call last):
          ...
        TypeError: Nie można użyć metody insert na obiekcie typu TagList.
        >>> T.reverse()
        Traceback (most recent call last):
          ...
        TypeError: Nie można użyć metody reverse na obiekcie typu TagList.
        >>> T *= 3
        Traceback (most recent call last):
          ...
        TypeError: Nie można użyć metody __imul__ na obiekcie typu TagList.
        >>> T[0] = "
wartość"
        Traceback (most recent call last):
          ...
        TypeError: Nie można użyć metody __setitem__ na obiekcie typu TagList.
        >>> T[0:2] = [17, 19]
        Traceback (most recent call last):
          ...
        TypeError: Nie można użyć metody __setslice__ na obiekcie typu TagList.
    "
""
    def _bad_method(name):
        def method(self, *args):
            raise TypeError("Nie można użyć metody %s na obiekcie typu TagList." % name)
        return method

    insert = _bad_method("insert")
    reverse = _bad_method("reverse")
    __imul__ = _bad_method("__imul__")
    __setitem__ = _bad_method("__setitem__")
    __setslice__= _bad_method("__setslice__")

    def __add__(self, iterable):
        return self.__class__(list.__add__(self, iterable))

    def __getslice__(self, i, j):
        return self.__class__(list.__getslice__(self, i, j))

    def __iadd__(self, iterable):
        self.extend(iterable)
        return self

    def __init__(self, tags=None):
        if isinstance(tags, basestring):
            self.extend(tags.split())
        elif isinstance(tags, list):
            self.extend(tags)

    def __repr__(self):
        return repr(self.__str__())

    def __str__(self):
        return ' '.join(self)

    def extend(self, iterable):
        for element in iterable:
            self.append(element)

    def append(self, object):
        if object not in self:
            if not isinstance(object, basestring):
                raise TypeError("Elementy listy muszą być stringami.")
            list.append(self, object)
        self.sort()

if __name__ == '__main__':
    import doctest
    doctest.testmod()

Przypisy

1 Prawie wszystkie. Może jeszcze wystąpić Traceback (innermost last):, ale doctest poradzi sobie, bez znaczenia którą wersję wybierzesz.

Autor: Michał Kwiatkowski
Data ostatniej modyfikacji: 30 kwietnia 2006
Licencja: Creative Commons Attribution-ShareAlike 2.5

Technorati tags: