Zanurkuj w Pythonie/Korzystanie z modułu timeit

Najważniejszą rzeczą, jaką powinniście wiedzieć na temat optymalizacji kodu w języku Python jest to, że nie powinniście pisać własnej funkcji mierzącej czas wykonania kodu.

Pomiar czasu wykonania niewielkich fragmentów kodu to zagadnienie niezwykle złożone. Jak dużo czasu procesora zużywa wasz komputer podczas działania tego kodu? Czy istnieją jakieś inne procesy działające w tym samym czasie w tle? Czy jesteście tego pewni? Na każdym nowoczesnym komputerze działają jakieś procesy w tle, niektóre przez cały czas a niektóre okresowo. Zadania w cronie zostają uruchomione w dokładnie określonych porach; usługi działające w tle co jakiś czas "budzą się", aby wykonać różne pożyteczne prace, takie jak sprawdzenie nowej poczty, połączenie się z serwerami typu "instant messaging", sprawdzanie, czy istnieją już aktualizacje zainstalowanych programów, skanowanie antywirusowe, sprawdzanie, czy w ciągu ostatnich 100 nanosekund do napędu CD została włożona płyta i tak dalej. Zanim rozpoczniecie testy z pomiarami czasu, wyłączcie wszystko i odłączcie komputer od sieci. Następnie wyłączcie to wszystko, o czym zapomnieliście za pierwszym razem, później wyłączcie usługę, która sprawdza, czy nie przyłączyliście się ponownie do sieci, a następnie...

Do tego dochodzą jeszcze różne aspekty wprowadzane przez mechanizm pomiaru czasu. Czy interpreter języka Python zapamiętuje raz wyszukane nazwy metod? Czy zapamiętuje bloki skompilowanego kodu? Wyrażenia regularne? Czy w waszym kodzie objawią się jakieś skutki uboczne, gdy uruchomicie go więcej niż jeden raz? Nie zapominajcie, że mamy tutaj do czynienia z małymi ułamkami sekundy, więc małe błędy w mechanizmie pomiaru czasu przełożą się na nienaprawialne zafałszowanie rezultatów tych pomiarów.

W społeczności Pythona funkcjonuje powiedzenie: "Python z bateriami w zestawie". Nie piszcie więc własnego mechanizmu mierzącego czas, bo Python od wersji 2.3 posiada służący do tego celu doskonały moduł o nazwie timeit.

Przykład 18.2. Wprowadzenie do modułu timeit

>>> import timeit
>>> t = timeit.Timer("soundex.soundex('Pilgrim')",
...     "import soundex")   #(1)
>>> t.timeit()              #(2)
8.21683733547
>>> t.repeat(3, 2000000)    #(3)
[16.48319309109, 16.46128984923, 16.44203948912]
  1. W module timeit zdefiniowana jest jedna klasa, Timer, której konstruktor przyjmuje dwa argumenty. Obydwa argumenty są napisami. Pierwszy z nich to instrukcja, której czas wykonania chcemy zmierzyć; w tym przypadku mierzymy czas wywołania funkcji soundex z modułu soundex z parametrem 'Pilgrim'. Drugi argument przekazywany do konstruktora obiektu Timer to instrukcja import, która ma przygotować środowisko do wykonywania instrukcji, której czas trwania mierzymy. Wewnętrzne działanie timeit polega na przygotowaniu wirtualnego, wyizolowanego środowiska, wykonaniu instrukcji przygotowujących (zaimportowaniu modułu soundex) oraz kompilacji i wykonaniu instrukcji poddawanej pomiarowi (wywołanie funkcji soundex).
  2. Najłatwiejsza rzecz, jaką można zrobić z obiektem klasy Timer, to wywołanie na nim metody timeit(), która wywołuje podaną funkcję milion razy i zwraca liczbę sekund, jaką zajęło to wywoływanie.
  3. Inną ważną metodą obiektu Timer jest repeat(), która pobiera dwa opcjonalne argumenty. Pierwszy argument to liczba określająca ile razy ma być powtórzony cały test; drugi argument to liczba określająca ile razy ma zostać wykonana mierzona instrukcja w ramach jednego testu. Obydwa argumenty są opcjonalne, a ich wartości domyślne to, odpowiednio, 3 oraz 1000000. Metoda repeat() zwraca listę zawierającą czas wykonania każdego testu wyrażony w sekundach.

Zauważcie, że repeat() zwraca listę czasów. Czasy te prawie nigdy nie będą identyczne; jest to spowodowane faktem, że interpreter języka Python nie zawsze otrzymuje taką samą ilość czasu procesora (oraz istnieniem różnych procesów w tle, których nie tak łatwo się pozbyć). Wasza pierwsza myśl może być taka: "Obliczmy średnią i uzyskajmy Prawdziwą Wartość".

W rzeczywistości takie podejście jest prawie na pewno złe. Testy, które trwały dłużej, nie trwały dłużej z powodu odchyleń w waszym kodzie albo kodzie interpretera języka; trwały dłużej, ponieważ w tym samym czasie w systemie działały w tle inne procesy albo z powodów niezależnych od interpretera języka, których nie można było całkowicie wyeliminować. Jeśli różnice w pomiarach czasu dla różnych testów różnią się o więcej niż kilka procent, wówczas są one zbyt znaczne, aby ufać takim pomiarom. W przeciwnym przypadku jako czas trwania testu należy przyjąć wartość najmniejszą, a inne wartości odrzucić.

Python posiada użyteczną funkcję min, która zwraca najmniejszą wartość z podanej w parametrze listy, a którą można w tym przypadku wykorzystać:

>>> min(t.repeat(3, 1000000))
8.22203948912