Сравнение быстродействия def и lambda-функций Python 3.x
Python MagazineИдея для кода
Читая pep8, я наткнулся на пункт об использовании анонимных функций — по версии pep8, они снижают читабельность, если использовать переменную с значением функции как функцию, лучше использовать def. Я решил сравнить def и lambda-функции по другому параметру — быстродействию. Предположительно, lambda, заточенный под однострочники, будет быстрее выполняться и создаваться. В этом исследовании я это проверю.
Библиотеки
Так как здесь будет много измерений времени, то несомненно, нам понадобится библиотека time, а также turtle, чтобы чертить разного рода графики. Я знаю, что это непрактично, но matprolib слишком долго (секунд 10) импортируется. Итак:
from turtle import * from time import time
Общие функции
В нашем коде для измерения быстродействия нужна соответствующая функция. Она будет главной для всех производных. Прежде всего, мы будем измерять время выполнения не один раз — слишком велика погрешность. Функция будет принимать в аргументы функцию, для которой проводится замер, а также количество повторений этой функции.
Для самого измерения мы будем использовать разницу во времени между началом выполнения и концом. Из описания складывается код:
def speed_test(func, n):
start = time()
for i in range(n):
func()
stop = time()
return stop - start
Всего у нас будет 2 диаграммы: полная и усредненная. В каждой по 2 графика: для def и lambda функций. Всего нам потребуется 4 черепахи.
Список значений для 1 и 2 графика очевиден — несколько результатов выполнения замера скорости. С 3 и 4 всё сложнее — нужно найти среднее арифметическое одного из 2 первых графиков. Дабы слишком не заморачиваться над тем, чтобы график никуда не вылезал, найдём разницу между каждым элементом каждого графика и средним значением между средними арифметическими из 1 и 2 графика. В итоге, на графике мы будем видеть не общее значение, а разницу.
Все графики занесём в общий словарь, чтобы не создавать много переменных. Словарь заранее объявлен за пределами функции.
def graph_data(func1, func2, mult1, mult2, arr_len):
l['l1'] = [func1(mult1)*mult2 for i in range(arr_len)]
l['l2'] = [func2(mult1)*mult2 for i in range(arr_len)]
l1_av = sum(l['l1']) // arr_len
l2_av = sum(l['l2']) // arr_len
av = sum((l1_av, l2_av)) / 2
l['l3'] = [l1_av - av for i in range(arr_len)]
l['l4'] = [l2_av - av for i in range(arr_len)]
for i in range(arr_len):
l['l1'][i] -= av
l['l2'][i] -= av
Функции для упрощения жизни
Кому захочется повторять одно и то действие, но с разными параметрами? Никому. Поэтому, я и написал некоторые вспомогательные функции, для рисования графика по заданным параметрам, для создания черепахи. Кстати, о последнем — черепахи тоже заносятся в общий словарь.
def draw(arr, t, x, mult=30):
n = len(arr)
t.up()
t.goto(-n*mult/2, 0)
for i, j in enumerate(arr):
t.goto(x+(-n*mult/2+i*mult), j)
t.down()
t.up()
def add_turtle(name, color='#000000', width=2):
t[name] = Turtle()
t[name].pencolor(color)
t[name].width(width)
t[name].hideturtle()
t[name].speed('fastest')
Производные функции
Слабонервным людям, ненавидящим многоуровневые вложения, не читать.
Для ранее описанных общих функций можно создавать бесконечно много проиводных. Для производной замера скорости структура такая:
def название(количество_повторений):
def функция_для_замера():
'''действия'''
return speed_test(функция_для_замера,
количество_повторений)
А производная для функции построения графика — это же самая функция с определенными аргументами.
Мы будем проверять скорость создания и скорость выполнения разного вида функций.
Вернёмся к первому. В случае проверки скорости создания функции, функция_для_замера() будет иметь одну цель — создать внутри себя def или lambda функцию. Её мы будем вызывать множество раз, и каждый раз она будет создавать одну и ту же функцию заново. Иными словами — функция второго уровня вложенности служит для многократного вызова и создания во время каждого функции третьего уровня вложенности.
Я мог бы сделать легче, но хотел сохранить структуру для всех производных функций.
Первые две производные — для создания пустых функций, возвращающих False. Для def я мог бы написать с использованием return или pass, но в lambda это невозможно.
def test_empty_def(n):
def adding_def_func():
def test(): return False
return speed_test(adding_def_func, n)
def test_empty_lambda(n):
def adding_lambda_func():
test = lambda: False
return speed_test(adding_lambda_func, n)
Следующие две — для таких же функций, но с простым выражением:
def test_def(n):
def adding_def_func():
def test(): return sum((2, 3, 4)) ** 0.5
return speed_test(adding_def_func, n)
def test_lambda(n):
def adding_lambda_func():
test = lambda: sum((2, 3, 4)) ** 0.5
return speed_test(adding_lambda_func, n)
Еще две — для оценки скорости их создания и скорости выполнения:
def test_def2(n):
def adding_def_func():
def test(): return sum((2, 3, 4)) ** 0.5
test()
return speed_test(adding_def_func, n)
def test_lambda2(n):
def adding_lambda_func():
test = lambda: sum((2, 3, 4)) ** 0.5
test()
return speed_test(adding_lambda_func, n)
Эти функции будут использованы в производных от graph_data:
def for_empty_func(arr_len):
graph_data(test_empty_def, test_empty_lambda, 10000, 20000, arr_len)
def for_one_eval_func(arr_len):
graph_data(test_def, test_lambda, 10000, 20000, arr_len)
def for_doing_func(arr_len):
graph_data(test_def2, test_lambda2, 10000, 20000, arr_len)
Алгоритм
Присвоим имя окну:
title('Сравнение def и lambda функций по скорости')
Создадим четыре черепахи для рисования графика:
t = {}
add_turtle('t1', '#c80000')
add_turtle('t2', '#00c800')
add_turtle('t3', '#c80000')
add_turtle('t4', '#00c800')
Определим длину диаграммы в вершинах:
arr_len = 20
Подготовим данные для графиков и построим их:
l = {}
for i in range(5):
производная_от_graph_data(arr_len)
draw(l['l1'], t['t1'], -300)
draw(l['l2'], t['t2'], -300)
draw(l['l3'], t['t3'], 300)
draw(l['l4'], t['t4'], 300)
Не забудем добавить событие закрытия окна:
exitonclick()
Окончательный алгоритм:
title('Сравнение def и lambda функций по скорости')
t = {}
add_turtle('t1', '#c80000')
add_turtle('t2', '#00c800')
add_turtle('t3', '#c80000')
add_turtle('t4', '#00c800')
arr_len = 20
l = {}
for i in range(5):
for_one_eval_func(arr_len)
draw(l['l1'], t['t1'], -300)
draw(l['l2'], t['t2'], -300)
draw(l['l3'], t['t3'], 300)
draw(l['l4'], t['t4'], 300)
exitonclick()
Тесты
Переходим к главному — что же быстрее? Зеленым на графике обозначены lambda, красным — def.
Первый тест — на скорость создания пустой (почти) функции:

Скорость создания пустой (почти) функции
Второй тест — на скорость создания функции с выражением:

Скорость создания функции с выражением
Третий тест — на скорость создания и выполнения:

Скорость создания и выполнения
Во всех случаях ведут lambda-функции.
Выводы
Для повышения читабельности в любом случае используйте def, ну а если скорость в приоритете — не используйте Python. Ну а если серьезно, то статья кому-то может оказаться полезной, ведь Python идеально подходит для некоторых задач, так почему-бы эти задачи не оптимизировать?