<-Powrót do strony głównej AISDI
<-Powrót do strony ze wskazówkami

Biblioteka pytest

Instalacja:

pip install pytest

pip install pytest-benchmark

Przykład użycia:

W celu pomiaru czasu można wykorzystać bibliotekę pytest. Przykład użycia na podstawie trzech sposobów łączenia ze sobą list:

def naive_concatenate(destination, source, destination_offset, source_offset):
while source_offset < len(source):
destination[destination_offset] = source[source_offset]
destination_offset += 1
source_offset += 1
return destination

def in_place_concatenate(destination, source, destination_offset, source_offset):
to_concat = source[source_offset:]
destination[destination_offset : destination_offset + len(to_concat)] = to_concat
return destination

def new_list_concatenate(destination, source, destination_offset, source_offset):
to_concat = source[source_offset:]
return (
destination[:destination_offset] + to_concat + destination[destination_offset + len(to_concat) :]
)

Pytest automatycznie uruchamia kod wiele razy, aby zminimalizować szum środowiskowy (np. ze stanu procesora, procesów w tle itp.), który może zniekształcać pomiary czasu wykonania. Wykonuje rozgrzewkę (cache warm-up) i odrzuca kilka pierwszych powolnych przebiegów (spowodowanych inicjalizacją lub brakiem pamięci podręcznej).

from concatenation import (
version,
naive_concatenate,
new_list_concatenate,
in_place_concatenate,
)
import pytest

def test_version():
assert version == "0.1.0"

@pytest.fixture
def concat_data():
destination_list = list(range(10000))
source_list = list(range(1000, 8000))
result = list(range(1500, 8000)) + list(range(6500, 10000))
return destination_list, source_list, result

def test_naive_concatenate(concat_data, benchmark):
assert benchmark(naive_concatenate, *concat_data[:2], 0, 500) == concat_data[-1]

def test_new_list_concatenate(concat_data, benchmark):
assert concat_data[0] != concat_data[-1]
assert benchmark(new_list_concatenate, *concat_data[:2], 0, 500) == concat_data[-1]

def test_in_place_concatenate(concat_data, benchmark):
assert concat_data[0] != concat_data[-1]
assert benchmark(in_place_concatenate, *concat_data[:2], 0, 500) == concat_data[-1]

Zapis wyników do pliku:

Ręczny pomiar czasu wykonania zazwyczaj daje pojedynczą liczbę, ale pytest-benchmark dostarcza szczegółowych statystyk:

Aby biblioteka zapisała wyniki do pliku należy podczas uruchamiania skryptu dodać opcję:

--benchmark-save=benchmark_results

Wówczas nastąpi stworzenie pliku JSON o nazwie benchmark_results w katalogu .benchmarks z pełnymi wynikami wszystkich testów. Przykładowy fragment pliku wynikowego JSON:

"benchmarks": [
    {
        "group": null,
        "name": "test_naive_concatenate",
        "fullname": "test_concatenation.py::test_naive_concatenate",
        "params": null,
        "param": null,
        "extra_info": {},
        "options": {
            "disable_gc": false,
            "timer": "perf_counter",
            "min_rounds": 5,
            "max_time": 1.0,
            "min_time": 5e-06,
            "warmup": false
        },
        "stats": {
            "min": 0.00022297000396065414,
            "max": 0.0008049440002650954,
            "mean": 0.00025703103677145417,
            "stddev": 3.915618459680731e-05,
            "rounds": 3115,
            "median": 0.0002536410029279068,
            "iqr": 1.9296248865430243e-05,
            "q1": 0.0002421549997961847,
            "q3": 0.00026145124866161495,
            "iqr_outliers": 149,
            "stddev_outliers": 118,
            "outliers": "118;149",
            "ld15iqr": 0.00022297000396065414,
            "hd15iqr": 0.0002904470020439476,
            "ops": 3890.5807351552485,
            "total": 0.8006516795430798,
            "iterations": 1
        }
    },

Posiadając wynikowy plik ze statystykami pomiaru czasów dla wszystkich testów można dokonać wykreślenia wykresów z pomocą biblioteki matplotlib

Pedantic mode + setup:

W niektórych przypadkach może okazać się, że użycie standarodwej funkcji benchmark jest niewystarczające, należy wówczas użyć funkcji benchmark.pedantic do bardziej precyzyjnego i kontrolowanego mierzenia wydajności kodu. Argument setup w tej funkcji pozwala na określenie kodu przygotowawczego, który zostanie wykonany przed każdym wywołaniem testowanego fragmentu kodu, ale nie będzie wliczany do czasu mierzonego benchmarku. Jest to szczególnie przydatne w sytuacjach:

Przykład:

Załóżmy że posiadamy klasę MyList która implementuje prostą listę z metodami do dołączania elementów na końcu listy (append), umieszczania elementów na zadanej pozycji (insert) oraz sprawdzania jej długości (__len__):

class MyList:
    def __init__(self):
        self.backend = []

    def append(self, item):
        self.backend.append(item)

    def insert(self, position, item):
        self.backend.insert(position, item)

    def __len__(self):
        return len(self.backend)

Jeśli chcemy przeprowadzić pomiary czasowe dla metod klasy MyList, narzucającą się, choć błędną ich implementacją będzie:

import pytest
from my_list import MyList


def test_append():
    test_list = MyList()
    assert len(test_list) == 0
    test_list.append(7)
    test_list.append(8)
    assert len(test_list) == 2


@pytest.mark.benchmark(group="append")
def test_benchmark_append(benchmark):
    test_list = MyList()
    benchmark(test_list.append, 7)

@pytest.mark.benchmark(group="insert")
def test_benchmark_insert(benchmark):
    test_list = MyList()
    benchmark(lambda x: test_list.insert(0, x), 7)

Obiekt test_list będący instancją klasy MyList przekazywany jest do funkcji benchmark. Biblioteka Pytest starając się wyznaczyć statystki czasowe, wywołuje metody klasy MyList wielokrotnie. Aby możliwe było wyznaczenie tych statystyk, zmierzony czas w kolejnych uruchomieniach musi oscylować wokół tych samych wartości, bądź zbiegać do pewnej konkretnej wartości czasowej. W przypadku funkcji testowych test_benchmark_append oraz test_benchmark_insert czas ten z każdym kolejnym uruchomieniem będzie rósł, gdyż kolejne iteracje testu będą przeprowadzane na tej samej instancji klasy MyList, a więc liczba elementów startowych listy w każdej kolejnej iteracji będzie o 1 większa. Rozwiązaniem tego problemu jest użycie funkcji testowej benchmark.pedantic z argumentem setup:

@pytest.mark.benchmark(group="append")
def test_benchmark_append_improved(benchmark):
    def setup():
        return (MyList(), 7), {}
    benchmark.pedantic(MyList.append, setup=setup)


@pytest.mark.benchmark(group="insert")
def test_benchmark_insert_improved(benchmark):
    def setup():
        return (MyList(), 0, 7), {}
    benchmark.pedantic(MyList.insert, setup=setup)

Pełen kod źródłowy przykładu.