Zapisz jako PDF

Transkrypt

Zapisz jako PDF
Spis treści
1 Boxcar
1.1 Model urządzenia pomiarowego
1.1.1 Etap 1: Przechowywanie danych
1.1.2 Etap 2: Obliczenia
1.1.3 Zadanie 1
1.1.4 Zadanie 2
1.1.5 Zadanie 3
1.2 Etap 3: Wykorzystanie enkapsulacji
1.3 Etap 4:tworzenie obiektu typu Boxcar, korzystając z iterowalnego kontenera z
danymi
1.3.1 Zadanie 4
2 Testowanie (na przykładzie klasy Wektor)
2.1 Wykonywalne przykłady w dokumentacji
2.2 Testowanie z wykorzystaniem docstringów
2.3 Sprawdzanie pokrycia kodu przez testy
2.3.1 Zadanie 1
2.3.2 Zadanie 2
2.4 Dodatki
3 Wczytywanie pliku raz jeszcze
3.1 Zadanie 1
3.2 Zadanie 2
3.3 Zadanie 3
3.4 Zadanie 4
3.5 Zadanie 5
3.6 Zadanie 6
Boxcar
Model urządzenia pomiarowego
Modelujemy urządzenie typu boxcar: układ pomiarowy dostarcza próbki (liczby zmiennoprzecinkowe) sekwencyjnie, a boxcar w każdym momencie przechowuje ostatnie
z nich i na
żądanie zwraca średnią i odchylenie standardowe.
Jeśli pomiar dopiero się zaczął i próbek jest mniej niż
liczbie próbek.
, obliczenia są wykonywane na mniejszej
Nasz kod napiszemy w postaci klasy.
Etap 1: Przechowywanie danych
Poszczególne próbki będziemy przechowywać w strukturze danych, która pozwala na dodanie
nowego elementu na początek sekwencji i usunięcie starego elementu z końca sekwencji.
W Pythonie jest już struktura, która zapewnia takie możliwości — zwykła lista ([], klasa list).
Zapamiętanie nowej próbki realizujemy jej na początek sekwencji próbek. Po dodaniu nowego
elementu sprawdzamy, czy nie mamy zbyt dużo próbek. Jeśli jest ich więcej niż , to usuwamy
najstarszy.
class Boxcar(object):
def __init__(self, N):
self.dane = []
self.N = N
def nowy_pomiar(self, pomiar):
self.dane.insert(, pomiar)
if len(self.dane) > self.N:
self.dane.pop()
Konstruując naszą klasę w ten sposób zrealizowaliśmy jeden z postulatów programowania
obiektowego: nasze dane nie są bezpośrednio widoczne dla użytkowników naszej klasy Boxcar.
Również za to w jaki sposób dane są przechowywane, za inicjalizację odpowiednich struktur
odpowiada wyłącznie klasa Boxcar. Użytkownik klasy jedynie tworzy obiekt Boxcar i wywołuje na
nim metodę nowy_pomiar. To właśnie enkapsulacja i rozdzielenie obowiązków.
Etap 2: Obliczenia
Do implementacji klasy Boxcar dodajmy metodę srednia.
def srednia(self):
suma = sum(self.dane)
liczba = len(self.dane)
return suma/liczba
Zadanie 1
Dodaj do klasy metodę dyspersja, która zwróci odchylenie
pamiętanych próbek od średniej.
Przypomnienie:
[Error parsing LaTeX formula. Error 7: error copying result:
cab6b473b3f773045d2a531597e7a237.png --> /var/www/html/edu/images/math/mathaf6aff98dec192d45c05ed0328e2e90e.png]
gdzie średnia
Zadanie 2
Zaprojektuj tekstową reprezentację obiektu. Dodaj do klasy metodę __str__, która wypisze
tekstową reprezentację obiektu klasy.
Zadanie 3
Sprawdź swoją implementację boxcara, wypełniając go liczbami losowymi z rozkładu płaskiego.
Etap 3: Wykorzystanie enkapsulacji
Lista nie jest idealną strukturą danych do przechowania pomiarów ponieważ operacja wstawienia
lub usunięcia elementu na początek listy jest operacją niewydajną.
Poszczególne próbki powinniśmy przechowywać w strukturze danych, która pozwala na szybkie
dodanie nowego elementu na początek sekwencji i usunięcie starego elementu z końca sekwencji.
W standardowej bibliotece Pythona jest już dostępna właśnie taka struktura: collections.deque.
Przykład użycia klasy deque:
>>> import collections
>>> q = collections.deque()
>>> q.appendleft(1)
>>> q.appendleft(2)
>>> q.appendleft(3)
>>> q
deque([3, 2, 1])
Zmodyfikujmy implementację klasy Boxcar tak, by wykorzystać deque zamiast list. Interfejs klasy
ani jej zachowanie nie mają prawa się zmienić!
class Boxcar(object):
def __init__(self, N):
self.dane = collections.deque()
self.N = N
def nowy_pomiar(self, pomiar):
self.dane.appendleft(pomiar)
if len(self.dane) > self.N:
self.dane.pop()
# reszta klasy pozostaje bez zmian!
Istnieją dwa podstawowe sposoby wykorzystania kodu jednej klasy przy tworzeniu drugiej —
dziedziczenie i kompozycja. W tym wypadku do konstrukcji klasy boxcar wykorzystaliśmy klasę
list a potem deque. Zrobiliśmy to poprzez kompozycję: dane są przechowywane w zmiennej
self.dane, natomiast klasa boxcar wywołuje metody na tym obiekcie w miarę potrzeby.
Alternatywą byłoby zastosowanie dziedziczenia — klasa boxcar dziedziczyłaby po list czy deque.
Te dwa rozwiązania możemy podsumować tak: albo boxcar ma kolejkę danych, albo boxcar jest
kolejką danych. Dziedziczenie ma tę wadę, że nasza nowa klasa ma zupełnie niepotrzebne metody,
np. reverse, odziedzione po klasie macierzystej. O ile funkcjonalność jest podobna przy jednym i
drugim rozwiązaniu, to kompozycja jest tutaj rozwiązaniem znacznie bardziej eleganckim. Jest to
zgodne z ogólną zasadą dobrego programowania obiektowego, że kompozycja jest lepsza niż
dziedziczenie.
Etap 4:tworzenie obiektu typu Boxcar, korzystając z iterowalnego kontenera z
danymi
Dokumentacja do obiektu collections.deque pokazuje, że tego typu obiekty można inicjalizować,
korzystając z obiektów, które są iterowalne (np. listy). Zmiana konstruktora klasy Boxcar tak, żeby
można było wykorzystać tę własność deque byłaby bardzo wygodna, bo umożliwiałaby tworzenie
obiektów typu Boxcar w oparciu o istniejące już kontenery z danymi.
Zadanie 4
Zmień konstruktor klasy Boxcar, tak by umożliwiał opcjonalne tworzenie obiektu w oparciu o
istniejący już kontener z danymi. Uwzględnij sytuację, w której kontener ten zawiera więcej
pomiarów niż założona długość bufora. Postaraj się zmienić kontruktor tak, by był jak najbardziej
odporny na próby podania nieodpowiednich danych.
Testowanie (na przykładzie klasy Wektor)
Mamy klasę Wektor. Jak wygodnie sprawdzić czy działa poprawnie? Odpowiedź: py.test.
Ponieważ nasz moduł definiujący Wektor jest króciusieńki, funkcje testujące możemy dodać do tego
samego modułu. W przypadku dużych programów tworzy się nawet osobny podkatalog (często
tests/), tutaj nie warto. Dopisuję więc fukcje sprawdzające mnożenie:
# ...
# definicja klasy Wektor
# ...
def test_mul():
w1 = Wektor(1, 2)
w2 = Wektor(3, 4)
assert w1 * w2 == 11
def test_mul_with_zeros():
w1 = Wektor(0, 2)
w2 = Wektor(3, 0)
assert w1 * w2 == 0
Aby sprawdzić nasz kod, używamy polecenia py.test:
$ py.test wektor.py -v’’’
============================= test session starts
=============================
python: platform linux2 -- Python 2.6.4 -- pytest-1.1.1 -- /usr/bin/python
test object 1: .../wektor/wektor.py
wektor.py:60: test_mul PASS
wektor.py:65: test_mul_zeros PASS
========================== 2 passed in 0.01 seconds
===========================
Działa!
Teraz dodajmy funkcję testującą dodawanie:
# ...
# definicja klasy Wektor
# ...
# testy mnożenia
# ...
def test_add():
w1 = Wektor(1, 2)
w2 = Wektor(3, 4)
assert w1 + w2 == Wektor(4, 6)
Ponownie odpalmy py.test:
$ py.test wektor.py -v’’’
============================= test session starts
=============================
python: platform linux2 -- Python 2.6.4 -- pytest-1.1.1 -- /usr/bin/python
test object 1: .../wektor/wektor.py
wektor.py:60: test_mul PASS
wektor.py:65: test_mul_zeros PASS
wektor.py:70: test_add FAIL
================================== FAILURES
===================================
__________________________________ test_add
___________________________________
def test_add():
w1 = Wektor(1, 2)
w2 = Wektor(3, 4)
>
assert w1 + w2 == Wektor(4, 6)
E
assert (<Wektor(1, 2) @140050567247696> + <Wektor(3, 4)
@140050567248016>) == <Wektor(4, 6) @140050567291216>
E
+ where <Wektor(4, 6) @140050567291216> = Wektor(4, 6)
wektor.py:73: AssertionError
===================== 1 failed, 2 passed in 0.06 seconds
======================
Nie działa! Dlaczego?
Oczywiście w wyrażeniu w1 + w2 == Wektor(4, 6) wykorzystujemy nie tylko operator +, ale
również operator ==, który wywołuje metodę __eq__ obiektu z lewej strony. Ponieważ klasa Wektor
nie zawiera definicji __eq__, to zostaje wywołana domyślna wersja tej metody. Domyślna wersja nie
wie nic o współrzędnych wektora i po prostu wykonuje porównanie tożsamości obiektów (z grubsza
is).
Aby test test_add przeszedł, mamy dwie możliwości. Możemy zmienić funkcję testującą tak, żeby
po prostu samemu sprawdzała równość współrzędnych:
# ...
# definicja klasy Wektor
# ...
# testy mnożenia
# ...
def test_add():
w1 = Wektor(1, 2)
w2 = Wektor(3, 4)
wynik = w1 + w2
assert wynik.a == 4 and wynik.b == 6
Niestety nie jest to rozwiązanie zbyt eleganckie, w szczególności musimy wiedzieć ile jest
współrzędnych oraz jak się nazywają.
Drugą opcją jest dodanie implementacji porównywania wektorów do klasy Wektor. Oczywiście ma to
sens tylko wtedy, gdy uważamy, że chcemy mieć porównywanie obiektów niezależnie od testów. Nie
należy dodawać funkcjonalności do klasy tylko po to by ułatwić pisanie testów.
# ...
# definicja klasy Wektor
# ...
def __eq__(self, other):
return self.a == other.a and self.b == other.b
#
# testy mnożenia
#
def test_add():
w1 = Wektor(1, 2)
w2 = Wektor(3, 4)
assert w1 + w2 == Wektor(4, 6)
Ponowne uruchomienie py.test skutkuje poprawnym wykonaniem trzech testów.
Wykonywalne przykłady w dokumentacji
Docstringi w Wektor są nieco zbyt zwięzłe. Bardzo wygodne dla użytkownika są proste wykonywalne
przykłady, które można przekleić do interpretera i trochę poeksperymentować. Zapisuje się je
w docstringu. Znaczki >>> poprzedzają fragmenty do wykonania, a wyniki wypisywane przez
Pythona się po prostu poprzedza spacjami. Łatwo się domyślić, że tak jest dlatego, że można po
prostu przekleić wykonywane przykłady z interaktywnej sesji, nie trzeba niczego wymyślać.
Spróbujmy:
class Wektor(object):
"""Dwuwymiarowy wektor.
>>> print Wektor(3, 5)
(3, 5)
>>> print Wektor(3, 5) + Wektor(-1, -1)
(2, 4)
"""
# ...
# reszta definicji klasy
#
Teraz osoba pisząca help(Wektor) od razu zorientuje się jak można użyć tej klasy. Polecenia
print użyłem zamiast po prostu wykonania wyrażenia tworzącego nowy Wektor po to, by uniknąć
nieistotnych szczegółów, np. wypisywania adresu przez Wektor.__repr__.
Testowanie z wykorzystaniem docstringów
Jeśli ktoś postanawia skorzystać z wykonywalnych poleceń zapisanych w docstringach, a nie działają
one tak jak należy, zazwyczaj nie jest szczęśliwy. Dlatego należy sprawdzać, czy zapisane przykłady
zachowują się nadal tak, jak zachowywały się kiedy osoba pisząca dokumentację wkleiła je do niej.
Służy do tego moduł doctest.
$ python -m doctest -v wektor.py
Trying:
print Wektor(3, 5)
Expecting:
(3, 5)
ok
Trying:
print Wektor(3, 5) + Wektor(-1, -1)
Expecting:
(2, 4)
ok
15 items had no tests:
# ...
# lista funkcji bez testów pominięta
# ...
1 items passed all tests:
2 tests in wektor.Wektor
2 tests in 16 items.
2 passed and 0 failed.
Test passed.
Również py.test pozwala na wykorzystanie wykonywalnych docstringów:
$ python -m doctest -v wektor.py
Takie wywołanie również skutkuje modułu doctest i daje identyczny efekt.
Sprawdzanie pokrycia kodu przez testy
Uważa się, że w dobrze przetestowanym projekcie, liczba linii kodu w fukcjach testujących to
100-200% linii kodu w głównej części. Na pewno nie jest łatwo odpowiedzieć na pytanie, czy nasze
testy faktycznie sprawdzają wszystko co należy. Niemniej często łatwo jest powiedzieć czego nie
sprawdzają — jeśli jakaś funkcja czy fragement kodu w trakcie testów nie jest wogóle uruchamiany,
to znaczy, że z całą pewnością nie jest testowany.
Do sprawdzania pokrycia kodu przez testy w połączeniu z py.test służy wtyczka figleaf:
$ py.test wektor.py -v
============================= test session starts
=============================
python: platform linux2 -- Python 2.6.4 -- pytest-1.1.1 -- /usr/bin/python
test object 1: .../wektor/wektor.py
wektor.py:60: test_mul PASS
wektor.py:65: test_mul_zeros PASS
wektor.py:70: test_add PASS
----------------------------------- figleaf ---------------------------------Writing figleaf data to .../wektor/.figleaf
Writing figleaf html to file://.../wektor/html
========================== 3 passed in 0.08 seconds
===========================
Wtyczka figleaf powoduje wygenerowanie plików HTML zawierających kopię kodu programu
z liniami oznaczonymi kolorami.
Zielony oznacza, że linia została wykonana w trakcie testów przynajmniej raz
Czerwony oznacza, że linia nie była wogóle użyta
Czarny oznacza, że linia nie zawiera wykonywalnego kodu, a np. komentarz czy docstring.
source file: /home/zbyszek/python/wektor/wektor.py
file stats: 33 lines, 24 executed: 72.7% covered
1. # -*- coding: utf-8 -*2. class Wektor(object):
3.
"""Dwuwymiarowy wektor."""
4.
5.
_ile_nas = 0
6.
7.
def __init__(self, a, b):
8.
self.a = a
9.
self.b = b
10.
Wektor._ile_nas += 1
11.
12.
def dlugosc(self):
13.
"""Zwraca liczbę, długość Wektora."""
14.
return (self.a**2 + self.b**2)**0.5
# ...
# reszta pominięta
#
Dzięki takiej pomocy możemy łatwo zobaczyć które fragmenty programu wymagają napisania więcej
testów. W tym przykładzie widać, że metoda Wektor.dlugosc jest takim fragmentem.
Zadanie 1
Dopisać program testujący powyższą klasę.
Zadanie 2
Napisz klasę Kwadrat. Obiekty tej klasy powinny:
przechowywać długość boku kwadratu
posiadać reprezentację napisową
posiadać metody zwracające pole i obwód kwadratu
operator dodawania zdefiniowany tak, aby obiekt powstały w wyniku dodawania dwóch
kwadratów miał pole równe sumie pól kwadratów składowych
Następnie napisz program testujący tęże klasę.
Dodatki
końcowa wersja kodu
pełen wynik działania figleaf poza tym wiki
Wczytywanie pliku raz jeszcze
Czy pamiętasz zadanie, w którym należało wczytać pliki i narysować widmo z zadanego kanału?
Zadanie 1
Zaprojektuj własną klasę, która zapewni obsługę pliku binarnego, zawierającego multipleksowany
sygnał o zadanej częstości próbkowania, liczbie kanałów, typie liczb. Zastanów się jakie jeszcze
mogą być parametry takiego pliku. Zastanów się, jakie są jej niezbędne pola i napisz jej konstruktor i
metodę __str__.
Zastanów się, jak najwygodniej będzie leniwemu użytkownikowi korzystać z obiektów
zaprojektowanej i zaimplementowanej przez siebie klasy. Zastanów się, czy nie warto jest wczytywać
pliku od razu w konstruktorze klasy (przekazywać nazwę pliku, częstość próbkowania i liczbę
kanałów, jako argumenty konstruktora klasy.
Zastanów się, jak zaprojektować metodę __str__, żeby nie wypisywała całego sygnału na ekran, ale
wyświetlała najważniejsze o nim informacje.
Zadanie 2
Do zaprojektowanej klasy dodaj następujące metody:
zwracającą cały sygnał z wybranego kanału (możesz w konstruktorze dodać opisy kanałów,
jeżeli dysponujesz opisem pliku i zawrzeć metodę, która zwraca sygnał z elektrody o
odpowiedniej nazwie),
zwracającą wybrany fragment sygnału z wybranej elektrody (np. sygnał od sekundy do
sekundy ),
zwracającą wybrany fragment sygnału z wszystkich elektrod (np. od sekundy do sekundy ),
rysującą widmo wybranego fragmentu sygnału z wybranej elektrody,
rysującą widmo fragmentu wszystkich sygnałów z wszystkich elektrod.
zwracającą fragmenty sygnału o zadanej długości z wybranej elektrody, zaczynające się od
kolejnych triggerów. Załóż, że w kanale z danym triggerem, wystąpienie triggera jest
zaznaczone 1 (wartość 0, oznacza brak triggera),
przygotuj się na możliwość występowania kanału specjalnego z triggerem (w takiej formie jak
na Pracowni Sygnałów Bioelektrycznych). Dodaj metody uśredniające fragmenty sygnału o
zadanej długości z wybranej elektrody, zaczynające się od kolejnych triggerów i rysujące ich
widmo. Zastanów się jak najlepiej przekazywać informację o tym, że któryś kanał jest kanałem
"specjalnym", np. triggerem. Do przetestowania cięcia danych wg triggera można użyć stąd
(wybrane pliki .raw), lub danych samodzielnie zebranych na pracowni,
stwórz klasę, dziedziczącą po powyższej, przedefiniowującą metody do wczytywania danych
tak, aby obsługiwać duże pliki. To znaczy nie trzymającą całych danych w pamięci, lecz
umiejącą wczytać wskazany fragment danych — wybrany kawałek, od sekundy t1, do t2, z
kanałów podanych w liście kanalów. We wczytywaniu pliku nie korzystaj z metody fromfile.
Otwórz plik metodą open, następnie wylicz do którego miejsca w pliku masz przejść używając
metody seek obiektu typu plik (podaj o ile bajtów ma się przesunąć) i wczytaj kolejne próbki
metodą read. Argumentem metody read jest liczba bajtów do wczytania, upewnij się więc, czy
pamiętasz, że liczby typu double zajmują 8 bajtów. Tak wczytane próbki musisz jeszcze
zamienić ze stringów na liczby typu double (pamiętając o porządku zapisu bajtów), korzystając
np. z funkcji unpack modułu struct.
Zadanie 3
Spróbuj przeciążyć metodę __init__, tak żeby można było konstruować obiekty, korzystając z
nazwy pliku (wczytując go w konstruktorze) albo z wektora z danymi.
Szkic prostszej metody:
def __init__(self, dane=None, plik=None):
if (dane is None) + (plik is None) != 1:
... #np. sys.exit(1)
Szkic bardziej skomplikowanej metody (wykorzystującej dekoratory):
class Dane(object):
def __init__(self, dane):
self.dane = dane
@classmethod
def fromfile(cls, name):
return cls(np.fromfile(dane))
Zadanie 4
Do klasy z dwóch pierwszych zadań dodaj przeciążony operator addycji (dodawania), który tworzyć
będzie obiekt z sygnałem, składającym się z obu sygnałów. Pamiętaj o sprawdzeniu, że sygnały są
tego samego typu — np. mają tyle samo kanałów i tę samą częstość próbkowania. Czy są jeszcze
jakieś operatory, które warto w tej sytuacji przeładować?
Zadanie 5
Zaprojektuj klasę zapewniającą obsługę pliku z danymi kalibrującymi z IV BCI Competition.
Przypomnij sobie opis tych danych. Napisz konstruktor, w którym będziesz wczytywać plik i metodę
__str__, która będzie zwracać najważniejsze informacje na temat danych zawartych w pliku.
Zadanie 6
Do zaprojektowanej przez siebie powyżej klasy, dodaj metody, które:
zwrócą sygnał z wybranego kanału,
zwroci listę wszystkich fragmentów sygnału odpowiadający danej klasie bodźca i danemu
kanałowi,
zwrócą uśredniony sygnał odpowiadający danej klasie bodźca i danemu kanałowi (uśredniamy
"triale"),
narysują i zapiszą do pliku widma z triali odpowiadających danej klasie bodźca i danemu
kanałowi (w nazwie pliku ma się znaleźć informacja o tym dla którego triala, której klasy i
którego kanału jest to widmo),
narysują widmo po uśrednieniu widm ze wszystkich triali dla danej klasy bodźca.