Używanie Pythona Timeit do pomiaru czasu kodu

Ten poradnik wprowadzi Cię w tajniki wykorzystania funkcji timeit z modułu timeit języka Python. Zdobędziesz wiedzę na temat mierzenia czasu wykonania zarówno prostych wyrażeń, jak i złożonych funkcji w Pythonie.

Pomiar czasu wykonywania fragmentów kodu jest niezwykle przydatny. Pozwala oszacować czas potrzebny na wykonanie danego fragmentu, a także zlokalizować te części kodu, które wymagają optymalizacji.

Rozpocznijmy od omówienia składni funkcji timeit. Następnie przejdziemy do praktycznych przykładów, aby zademonstrować, jak można jej użyć do pomiaru czasu wykonania bloków kodu i funkcji w Pythonie. Zatem, do dzieła!

Wykorzystanie funkcji timeit w Pythonie

Moduł timeit jest integralną częścią standardowej biblioteki Pythona, a jego import jest bardzo prosty:

import timeit

Poniżej prezentowana jest składnia funkcji timeit, którą znajdziemy w module o tej samej nazwie:

timeit.timeit(stmt, setup, number)

Gdzie:

  • stmt oznacza fragment kodu, którego czas wykonania chcemy zmierzyć. Może to być prosty ciąg znaków w Pythonie, ciąg wieloliniowy lub nazwa funkcji do wywołania.
  • setup, jak sama nazwa wskazuje, to kod, który musi zostać wykonany tylko raz, zazwyczaj jako przygotowanie do uruchomienia stmt. Na przykład, jeśli mierzymy czas tworzenia tablicy NumPy, import biblioteki NumPy będzie kodem setup, a samo tworzenie tablicy będzie instrukcją (stmt).
  • Parametr number określa, ile razy stmt ma zostać uruchomiony. Domyślną wartością jest milion (1000000), ale można ustawić dowolną inną liczbę.

Teraz, kiedy już znamy składnię timeit(), przejdźmy do praktycznych przykładów.

Pomiar czasu prostych wyrażeń w Pythonie

W tej sekcji spróbujemy zmierzyć czas wykonania prostych wyrażeń w Pythonie za pomocą timeit.

Uruchom interaktywną konsolę Pythona (REPL) i przetestuj poniższe przykłady. Zmierzymy czas wykonania operacji potęgowania i dzielenia całkowitego dla 10000 i 100000 powtórzeń.

Zauważ, że instrukcję do pomiaru czasu przekazujemy jako ciąg znaków Pythona, używając średnika do rozdzielenia poszczególnych wyrażeń.

>>> import timeit
>>> timeit.timeit('3**4;3//4',number=10000)
0.0004020999999738706

>>> timeit.timeit('3**4;3//4',number=100000)
0.0013780000000451764

Użycie timeit z wiersza poleceń

Funkcję timeit można również wywoływać bezpośrednio z wiersza poleceń. Poniżej znajduje się ekwiwalent wywołania funkcji timeit z poziomu terminala:

$ python -m timeit -n [number] -s [setup] [stmt]
  • python -m timeit oznacza, że uruchamiamy timeit jako główny moduł.
  • Opcja -n w wierszu poleceń określa, ile razy kod powinien zostać wykonany. Jest to odpowiednik argumentu number w funkcji timeit().
  • Można użyć opcji -s do zdefiniowania kodu przygotowawczego (setup).

Poniżej przedstawiamy poprzedni przykład, używając ekwiwalentu z wiersza poleceń:

$ python -m timeit -n 100000 '3**4;3//4'
100000 loops, best of 5: 35.8 nsec per loop

W tym przykładzie mierzymy czas wykonania wbudowanej funkcji len(). Inicjalizacja ciągu znaków jest kodem instalacyjnym przekazanym za pomocą opcji -s.

$ python -m timeit -n 100000 -s "string_1 = 'coding'" 'len(string_1)'
100000 loops, best of 5: 239 nsec per loop

W wynikach otrzymujemy czas wykonania dla najlepszego z 5 powtórzeń. Co to oznacza? Podczas uruchamiania timeit z wiersza poleceń opcja powtarzania -r ma domyślną wartość 5. Oznacza to, że wykonanie instrukcji (stmt) określoną liczbę razy jest powtarzane pięć razy, a zwracany jest najlepszy z uzyskanych czasów.

Analiza metod odwracania ciągów znaków za pomocą timeit

Podczas pracy z ciągami znaków w Pythonie, często zachodzi potrzeba ich odwrócenia. Najczęściej stosowane metody odwracania ciągów znaków to:

  • Wykorzystanie mechanizmu wycinania (slicing).
  • Użycie funkcji reversed() i metody join().

Odwracanie ciągów znaków za pomocą mechanizmu wycinania (slicing)

Przyjrzyjmy się, jak działa mechanizm wycinania i jak można go użyć do odwrócenia ciągu znaków. Składnia jakiś-ciąg[start:stop] zwraca fragment ciągu, zaczynając od indeksu start do indeksu stop-1. Rozważmy poniższy przykład.

Weźmy pod uwagę ciąg znaków „Python”. Ma on długość 6, a indeksy poszczególnych znaków to 0, 1, 2, 3, 4 i 5.

>>> string_1 = 'Python'

Jeśli określimy zarówno wartość startową, jak i końcową, otrzymamy fragment ciągu rozciągający się od start do stop-1. Na przykład, string_1[1:4] zwróci 'yth’.

>>> string_1 = 'Python'
>>> string_1[1:4]
'yth'

Jeśli wartość startowa nie zostanie określona, domyślnie przyjmowana jest wartość zero, a wycinek zaczyna się od indeksu zero i rozciąga się do indeksu stop-1.

W tym przypadku wartość stop wynosi 3, więc wycinek zaczyna się od indeksu 0 i kończy na indeksie 2.

>>> string_1[:3]
'Pyt'

Jeśli nie podamy indeksu stop, wycinek zacznie się od indeksu startowego (1) i rozciągnie się do końca ciągu.

>>> string_1[1:]
'ython'

Pominięcie zarówno wartości startowej, jak i końcowej zwróci wycinek zawierający cały ciąg znaków.

>>> string_1[::]
'Python'

Możemy również tworzyć wycinki z określoną wartością kroku. Ustawmy wartości start, stop i krok odpowiednio na 1, 5 i 2. Uzyskamy wycinek ciągu znaków zaczynający się od indeksu 1 i rozciągający się do indeksu 4 (bez włączenia punktu końcowego 5), który zawiera co drugi znak.

>>> string_1[1:5:2]
'yh'

Używając ujemnej wartości kroku, możemy uzyskać wycinek zaczynający się od końca ciągu znaków. Przy kroku ustawionym na -2, string_1[5:2:-2] da nam następujący wycinek:

>>> string_1[5:2:-2]
'nh'

Aby uzyskać odwróconą kopię ciągu znaków, pomijamy wartości startową i końcową i ustawiamy krok na -1, jak pokazano poniżej:

>>> string_1[::-1]
'nohtyP'

Podsumowując, ciąg[::-1] zwraca odwróconą kopię ciągu.

Odwracanie ciągów znaków za pomocą wbudowanych funkcji i metod

Wbudowana funkcja reversed() w Pythonie zwraca odwrotny iterator po elementach ciągu.

>>> string_1 = 'Python'
>>> reversed(string_1)
<reversed object at 0x00BEAF70>

Możemy przejść przez ten odwrotny iterator za pomocą pętli for:

for char in reversed(string_1):
    print(char)

I uzyskać dostęp do znaków ciągu w odwrotnej kolejności.

# Output
n
o
h
t
y
P

Następnie możemy wywołać metodę join() na odwrotnym iteratorze, stosując składnię: .join(reversed(jakiś-ciąg)).

Poniższy kod przedstawia kilka przykładów, gdzie separatorem jest myślnik i spacja.

>>> '-'.join(reversed(string1))
'n-o-h-t-y-P'
>>> ' '.join(reversed(string1))
'n o h t y P'

W tym przypadku nie chcemy żadnego separatora, ustawiamy separator na pusty ciąg, aby otrzymać odwróconą kopię ciągu:

>>> ''.join(reversed(string1))
'nohtyP'

Zatem .join(reversed(some-string)) zwraca odwróconą kopię ciągu znaków.

Porównanie czasów wykonania za pomocą timeit

Do tej pory poznaliśmy dwa sposoby na odwrócenie ciągu znaków w Pythonie. Który z nich jest szybszy? Sprawdźmy to!

W poprzednim przykładzie, gdy mierzyliśmy czas prostych wyrażeń, nie mieliśmy kodu przygotowawczego. Teraz, gdy odwracamy ciąg znaków, operacja odwrócenia będzie wykonywana określoną liczbę razy, a kod przygotowawczy będzie inicjalizacją ciągu, która zostanie wykonana tylko raz.

>>> import timeit
>>> timeit.timeit(stmt="string_1[::-1]", setup = "string_1 = 'Python'", number = 100000)
0.04951830000001678
>>> timeit.timeit(stmt = "''.join(reversed(string_1))", setup = "string_1 = 'Python'", number = 100000)
0.12858760000000302

Przy tej samej liczbie powtórzeń, podejście z użyciem wycinania ciągu jest szybsze niż metoda wykorzystująca join() i reversed().

Pomiar czasu wykonania funkcji w Pythonie za pomocą timeit

W tej sekcji nauczymy się, jak mierzyć czas wykonania funkcji w Pythonie za pomocą timeit. Rozważmy funkcję hasDigit, która przyjmuje listę ciągów i zwraca listę tych, które zawierają co najmniej jedną cyfrę.

def hasDigit(somelist):
     str_with_digit = []
     for string in somelist:
         check_char = [char.isdigit() for char in string]
         if any(check_char):
            str_with_digit.append(string)
     return str_with_digit

Chcemy teraz zmierzyć czas wykonania tej funkcji za pomocą timeit.

Najpierw musimy zidentyfikować instrukcję (stmt), którą chcemy zmierzyć. Będzie to wywołanie funkcji hasDigit() z listą ciągów jako argumentem. Następnie musimy zdefiniować kod przygotowawczy (setup). Jak myślisz, jaki powinien być kod setup?

Aby wywołanie funkcji przebiegło poprawnie, kod przygotowawczy powinien zawierać:

  • Definicję funkcji hasDigit().
  • Inicjalizację listy ciągów znaków, która będzie argumentem funkcji.

Zdefiniujmy kod przygotowawczy w łańcuchu setup, jak pokazano poniżej:

setup = """
def hasDigit(somelist):
    str_with_digit = []
    for string in somelist:
      check_char = [char.isdigit() for char in string]
      if any(check_char):
        str_with_digit.append(string)
    return str_with_digit
thislist=['puffin3','7frost','blue']
     """

Możemy teraz wykorzystać funkcję timeit i uzyskać czas wykonania funkcji hasDigit() dla 100000 powtórzeń.

import timeit
timeit.timeit('hasDigit(thislist)',setup=setup,number=100000)
# Output
0.2810094920000097

Podsumowanie

Nauczyłeś się, jak używać funkcji timeit w Pythonie do pomiaru czasu wykonania wyrażeń, funkcji i innych obiektów wywoływalnych. Ta wiedza pozwoli Ci przeprowadzać testy porównawcze kodu, analizować różne implementacje tej samej funkcji, i wiele więcej.

Podsumowując to, czego się nauczyliśmy, możemy użyć funkcji timeit() stosując składnię timeit.timeit(stmt=…,setup=…,number=…). Alternatywnie, możemy uruchomić timeit z wiersza poleceń, aby zmierzyć czas wykonania krótkich fragmentów kodu.

Następnym krokiem może być zapoznanie się z innymi pakietami do profilowania kodu w Pythonie, takimi jak line-profiler i memprofiler, które umożliwiają profilowanie kodu pod względem czasu i zużycia pamięci.

Kolejnym krokiem może być nauka obliczania różnic czasu w Pythonie.