2
\$\begingroup\$

Hello everyone.

I wrote my own test module, which is responsible for measuring the timing of algorithms, I used it mainly for sorting algorithms this is so to say a very early beta version, which I would like to expand with various functionalities, in general I would like to take care of the appearance of the code. I look forward to your opinions and comments, possibly your own ideas.

So here is my testing_module code:

import random
import matplotlib.pyplot as plt

accident = ["x", "random.randint(1, 100)", "x"]
history = ["Optimistic", "Average", "Pessimistic"]

def make_data(numbers, y):
    data = []
    for element in numbers:
        data.append([eval(accident[y]) for x in range(element)])
    return data

def make_plot(numbers, measures, name):
    for x in range(3):
        plt.plot(numbers, measures[x], label = history[x])
    plt.title(name)
    plt.legend()
    plt.show()

def test(method, numbers, measures):
    name_function = method.__name__
    for x in range(3):
        if x != 2:
            for element in make_data(numbers, x):
                method(element, x)
        else:
            for element in make_data(numbers, x):
                element.reverse()
                method(element, x)
                
    make_plot(numbers, measures, name_function)

And this is how i was using it in my code:

import testing_module as test
import time

measures = [[], [], []]
numbers = [10, 100, 300, 500, 700]

def insertion_sort(array, x=0):
    start = time.time()
    for j in range(1, len(array)):
        key = array[j]
        i = j - 1
        while (i > -1) and key < array[i]:
            array[i + 1] = array[i]
            i = i - 1
        array[i + 1] = key
    end = time.time()
    measures[x].append(end - start)

test.test(insertion_sort, numbers, measures)

Thanks in advance for any tips and ideas how to improve my solution. Have a great day!

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

If I had to summarise this code, it would be: magical, spooky and puzzling. It works (which is a great start!) but you can make life easier on yourself and your maintainers.

You've invoked the root of all eval. Don't do this. There are many, many alternatives; the one I'll show uses simple polymorphism.

Many of your variable names need work. element is actually equivalent to your array; numbers is actually lengths; accident is really a sequence of test series generators.

Reduce your reliance on inferred figures and axes via the plt. functions, and instead use your figure and axis objects explicitly.

Avoid forcing a plt.show() inside of your make_plot; let the caller decide what to do with the figure.

Do not use plot(). For these data, Optimistic is basically flat. Use loglog instead.

Do not do timing and measures construction on the inside of your UUT (unit under test).

Do not call test.test from with the global namespace; use a __main__ guard. Similarly, do not leave measures and numbers in the global namespace.

Introduce PEP484 type hints.

Avoid calling reverse(). Your generated test series are just as easily made via range calls, which will be more efficient and avoid mutation.

Suggested

from random import randrange
from time import time
from typing import Callable, Iterable, List, Iterator, Sequence, Tuple

import matplotlib.pyplot as plt


Method = Callable[[List[int]], None]


class History:
    def __init__(self, lengths: Sequence[int], method: Method) -> None:
        self.measures: Tuple[float] = tuple(self.test(lengths, method))

    @staticmethod
    def make_data(length: int) -> Iterable[int]:
        raise NotImplementedError()

    @classmethod
    def test(cls, lengths: Sequence[int], method: Method) -> Iterator[float]:
        for length in lengths:
            array = list(cls.make_data(length))
            start = time()
            method(array)
            end = time()
            yield end - start


class Optimistic(History):
    @staticmethod
    def make_data(length: int) -> Iterable[int]:
        return range(length)


class Pessimistic(History):
    @staticmethod
    def make_data(length: int) -> Iterable[int]:
        return range(length-1, -1, -1)


class Average(History):
    @staticmethod
    def make_data(length: int) -> Iterable[int]:
        for _ in range(length):
            yield randrange(1000)


def make_plot(
    lengths: Sequence[int],
    histories: Iterable[History],
    name: str,
) -> plt.Figure:
    fig, ax = plt.subplots()
    for history in histories:
        ax.loglog(lengths, history.measures, label=type(history).__name__)
    ax.set_title(name)
    ax.legend()
    return fig


def test(
    method: Method,
    lengths: Sequence[int],
) -> Iterator[History]:
    for history_t in (Optimistic, Average, Pessimistic):
        yield history_t(lengths, method)


def insertion_sort(array: List[int]) -> None:
    for j in range(1, len(array)):
        key = array[j]
        i = j - 1
        while (i > -1) and key < array[i]:
            array[i + 1] = array[i]
            i = i - 1
        array[i + 1] = key


def main() -> None:
    lengths = (10, 100, 300, 500, 700)
    histories = test(method=insertion_sort, lengths=lengths)
    make_plot(
        lengths=lengths, histories=histories,
        name=insertion_sort.__name__,
    )
    plt.show()


if __name__ == '__main__':
    main()

plot

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Perhaps useful if OP is not used to working with figure/axes: pyplot vs object-oriented interface \$\endgroup\$
    – tdy
    Commented Nov 4, 2021 at 23:18
  • \$\begingroup\$ I was testing it right now on the Python 3.9.8 and results are pretty weird... Everything's fine on 3.6.7 \$\endgroup\$ Commented Nov 14, 2021 at 21:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.