Zanurkuj w Pythonie/Programowanie funkcyjne - wszystko razem

Dowiedzieliście się już wystarczająco dużo, by móc odczytać pierwszych siedem linii kodu z przykładu podanego na początku rozdziału, w którym odczytywane są pliki w katalogu a następnie importowane wybrane spośród nich moduły.

Przykład 16.16. Funkcja regressionTest

def regressionTest():
    path = os.path.abspath(os.path.dirname(sys.argv[0]))   
    files = os.listdir(path)                               
    test = re.compile("test\.py$", re.IGNORECASE)          
    files = filter(test.search, files)                     
    filenameToModuleName = lambda f: os.path.splitext(f)[0]
    moduleNames = map(filenameToModuleName, files)         
    modules = map(__import__, moduleNames)                 
load = unittest.defaultTestLoader.loadTestsFromModule  
return unittest.TestSuite(map(load, modules))

Spójrzmy na ten kod w sposób interaktywny, linia po linii. Załóżmy, że katalogiem bieżącym jest c:\diveintopython\py, w którym znajdują się przykłady dołączone do tej książki, z omawianym w tym rozdziale skryptem włącznie. Jak widzieliśmy w podrozdziale 16.2 Znajdowanie ścieżki, nazwa katalogu, w którym znajduje się skrypt, trafi do zmiennej path, spróbujmy więc tę część zakodować na sztywno, po czym dopiero przejść dalej.

Przykład 16.17. Krok 1: Pobieranie wszystkich plików

>>> import sys, os, re, unittest
>>> path = r'c:\diveintopython\py'
>>> files = os.listdir(path)                               
>>> files #(1)
['BaseHTMLProcessor.py', 'LICENSE.txt', 'apihelper.py', 'apihelpertest.py',
'argecho.py', 'autosize.py', 'builddialectexamples.py', 'dialect.py',
'fileinfo.py', 'fullpath.py', 'kgptest.py', 'makerealworddoc.py',
'odbchelper.py', 'odbchelpertest.py', 'parsephone.py', 'piglatin.py',
'plural.py', 'pluraltest.py', 'pyfontify.py', 'regression.py', 'roman.py', 'romantest.py',
'uncurly.py', 'unicode2koi8r.py', 'urllister.py', 'kgp', 'plural', 'roman',
'colorize.py']

  1. files jest listą nazw wszystkich plików i katalogów znajdujących się w katalogu, z którego pochodzi skrypt. (Jeśli wcześniej uruchamialiście już jakieś przykłady, na liście możecie również zauważyć pliki .pyc)

Przykład 16.18. Krok 2: Filtrowanie w celu znalezienia interesujących plików

>>> test = re.compile("test\.py$", re.IGNORECASE)           #(1)
>>> files = filter(test.search, files)                      #(2)
>>> files                                                   #(3)
['apihelpertest.py', 'kgptest.py', 'odbchelpertest.py', 'pluraltest.py', 'romantest.py']
  1. Wyrażenie regularne zostanie dopasowane do każdego napisu zakończonego na test.py. Zauważcie, że kropka musi zostać poprzedzona sekwencją unikową; w wyrażeniach regularnych kropka oznacza "dopasuj dowolny znak", jednak nam zależy na dosłownym dopasowaniu znaku kropki.
  2. Skompilowane wyrażenie regularne działa jak funkcja, a więc możemy jej użyć do przefiltrowania długiej listy nazw plików i katalogów, dzięki czemu uzyskamy listę nazw, do których zostało dopasowane wyrażenie.
  3. Otrzymaliśmy więc listę skryptów będących testami jednostkowymi, ponieważ tylko one mają nazwę JAKASNAZWAtest.py.

Przykład 16.19. Krok 3: Odwzorowanie nazw plików na nazwy modułów

>>> filenameToModuleName = lambda f: os.path.splitext(f)[0] #(1)
>>> filenameToModuleName('romantest.py')                    #(2)
'romantest'
>>> filenameToModuleName('odchelpertest.py')
'odbchelpertest'
>>> moduleNames = map(filenameToModuleName, files)          #(3)
>>> moduleNames                                             #(4)
['apihelpertest', 'kgptest', 'odbchelpertest', 'pluraltest', 'romantest']
  1. Jak widzieliśmy w podrozdziale 4.7 Wyrażenia lambda, lambda pozwala na szybkie zdefiniowanie jednolinijkowych funkcji w locie. Tutaj funkcja lambda pobiera nazwę pliku wraz z rozszerzeniem i zwraca część nazwy bez rozszerzenia, używając do tego funkcji os.path.splitext z biblioteki standardowej, którą poznaliśmy w przykładzie 6.17 "Rozdzielanie ścieżek" w podrozdziale Praca z katalogami.
  2. filenameToModuleName jest funkcją. W porównaniu ze zwykłymi funkcjami, które tworzy się przy pomocy instrukcji def, w funkcjach lambda nie ma niczego magicznego. Możemy wywołać filenameToModuleName jak każdą inną funkcję, a robi ona dokładnie to, czego potrzebujemy: odcina rozszerzenie z napisu przekazanego jej w parametrze wejściowym.
  3. Tutaj możemy wywołać tę funkcję na każdej nazwie pliku znajdującej się na liście plików będących testami jednostkowymi. Używamy do tego funkcji map.
  4. W wyniku otrzymujemy to, czego oczekiwaliśmy: listę nazw modułów.

Przykład 16.20. Krok 4: Odwzorowanie nazw modułów na moduły

>>> modules = map(__import__, moduleNames)                  #(1)
>>> modules                                                 #(2)
[<module 'apihelpertest' from 'apihelpertest.py'>,
<module 'kgptest' from 'kgptest.py'>,
<module 'odbchelpertest' from 'odbchelpertest.py'>,
<module 'pluraltest' from 'pluraltest.py'>,
<module 'romantest' from 'romantest.py'>]
>>> modules[-1]                                             #(3)
<module 'romantest' from 'romantest.py'>
  1. Jak widzieliśmy w podrozdziale 16.6 Dynamiczne importowanie modułów, w celu odwzorowania listy nazw modułów (napisów) na właściwe moduły (obiekty wywoływalne, do których można mieć dostęp jak do jakichkolwiek innych modułów), można użyć funkcji map oraz __import__.
  2. modules to teraz lista modułów, do których można mieć taki sam dostęp, jak do jakichkolwiek innych modułów.
  3. Ostatnim modułem na liście jest moduł romantest, tak jak gdybyśmy napisali: import romantest

Przykład 16.21. Krok 5: Ładowanie modułów do zestawu testów

>>> load = unittest.defaultTestLoader.loadTestsFromModule  
>>> map(load, modules)                     #(1)
[<unittest.TestSuite tests=[
  <unittest.TestSuite tests=[<apihelpertest.BadInput testMethod=testNoObject>]>,
  <unittest.TestSuite tests=[<apihelpertest.KnownValues testMethod=testApiHelper>]>,
  <unittest.TestSuite tests=[
    <apihelpertest.ParamChecks testMethod=testCollapse>, 
    <apihelpertest.ParamChecks testMethod=testSpacing>]>, 
    ...
  ]
]
>>> unittest.TestSuite(map(load, modules)) #(2)

  1. To są prawdziwe obiekty modułów. Nie tylko mamy do nich dostęp taki, jak do innych modułów i możemy tworzyć instancje klas oraz wywoływać funkcje, mamy również możliwość introspekcji (wglądu) w moduł, której możemy użyć przede wszystkim do tego, aby dowiedzieć się, jakie klasy i funkcje dany moduł posiada. To właśnie robi metoda loadTestsFromModule: dokonuje introspekcji, a następnie dla każdego modułu zwraca obiekt unittest.TestSuite. Każdy taki obiekt zawiera listę obiektów unittest.TestSuite, po jednym dla każdej klasy dziedziczącej po TestCase zdefiniowanej w module. Każdy z obiektów na tej liście zawiera z kolei listę metod testowych zdefiniowanych w klasie testowej.
  2. Na końcu umieszczamy listę obiektów TestSuite wewnątrz jednego, dużego zestawu testów. Moduł unittest nie ma problemów z przechodzeniem po drzewie zestawów testowych zagnieżdżonych w zestawach testowych; dotrze on do każdej metody testowej i ją wywoła, sprawdzając, czy przeszła, czy nie, a następnie przejdzie do kolejnej metody.

Moduł unittest zwykle przeprowadza za nas proces introspekcji. Czy pamiętacie magiczną funkcję unittest.main(), którą wywoływały poszczególne moduły, aby odpalić wszystkie znajdujące się w nich testy? Metoda unittest.main() w rzeczywistości tworzy instancję klasy unittest.TestProgram, która z kolei tworzy instancję unittest.defaultTestLoader, służącą do załadowania modułu, z którego została wywołana. (Skąd jednak ma referencję do modułu, z którego została wywołana, jeśli nie dostała jej od nas? Otóż dzięki równie magicznemu poleceniu __import__('__main__'), które dynamicznie importuje wykonywany właśnie moduł. Mógłbym napisać całą książkę na temat trików i technik używanych w module unittest, ale chyba bym jej nie skończył.

Przykład 16.22. Krok 6: Powiedzieć modułowi unittest, aby użył naszego zestawu testowego

if __name__ == "__main__":                   
    unittest.main(defaultTest="regressionTest") #(1)
  1. Zamiast pozwalać modułowi unittest wykonać całą magię za nas, większość zrobiliśmy sami. Utworzyliśmy funkcję (regressionTest), która sama importuje moduły i woła unittest.defaultTestLoader, a następnie utworzyliśmy duży zestaw testów. Teraz potrzebujemy jedynie, aby unittest, zamiast szukać testów i budować zestaw testowy w standardowy sposób, uruchomił funkcję regressionTest, która zwróci gotwy do użycia obiekt testSuite.