Идея для кода

Читая 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 функцию. Эту функцию мы будем вызывать множество раз, и каждый раз она будет создавать одну и ту же функцию заново. Иными словами - функция второго уровня вложенности служит для многократного вызова и создания во время каждого функции 3 уровня вложенности. Надеюсь, вы меня поняли.

Я мог бы сделать легче, но хотел сохранить структуру для всех производных функций.

Первые две производные - для создания пустых функций, возвращаюших 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 функций по скорости')

Создадим 4 черепахи для рисования графика:

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()
Полный код
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

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)

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 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 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 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):
    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 идеально подходит для некоторых задач, так почему-бы эти задачи не оптимизировать?

P.S. После проверки в условиях, близких к идеальным, результаты сравнялись. Как писали в комментариях, def и lambda - лишь синтаксический сахар, но в неидеальных условиях разница есть.

Комментарии (15)



  1. vkni
    01.01.2022 08:29
    +12

    Всё-таки, у Python'а совершенно неочевидная семантика. Почему вообще возникает мысль, что эти "def" и "lambda" как-то должны отличаться, а не быть синтаксическим сахаром друг друга? :-)


    1. MaryRabinovich
      01.01.2022 14:52

      потому что, скажем, на javascript анонимные функции и обычные - две больших разницы. Тут вроде тоже речь об обычных и анонимных.


      1. valis
        01.01.2022 20:01

        Вы имеете в виду может быть стрелочные а не анонимные? Анонимной может быть не только стрелочная функция.


        1. MaryRabinovich
          01.01.2022 21:45

          нет, я про разницу типа "определяется внутри блока или на весь объемлющий файл", например. Если вы где-то внизу джаваскриптовского файла напишете
          let MyFunc = function() {
          // какая-то функциональность
          }
          то вызывать её вверху файла из хэндла MyFunc вы не сможете. Даже если сам этот хендл как переменная определён выше, файл будет знать, что там лежит функция только ниже определения.

          Если же вы внизу файла напишете то же самое, только начнёте с function MyFunc() {тут та же функциональность}, такая всплывёт наверх.

          ЗЫ но на питоне, на самом деле, вверх вообще ничего не всплывает (кроме как в определениях классов), так что моё сравнение с джаваскриптом не очень. Питон же интерпретируется по мере прохода текста.


      1. vkni
        01.01.2022 21:11

        Это лишь поднимает вопрос - а там-то зачем? Я ещё пойму про implicit return (хотя я бы старался унифицировать), но вот остальное-то нафига сделано по-разному?


  1. MaryRabinovich
    01.01.2022 14:49

    Вопрос, а нет ли проблемы в том, что def вы используете не вполне типовым для него образом. Это именно вопрос, я ответа не знаю, так как я больше по другим языкам, питоном только с детьми занимаюсь.

    Ваш синтаксис - это def в одну строчку, внутри другой функции. Которая далее вызывается, как я понимаю, единожды.

    В реальности же использование def - это не "создание (и использование)", а "создание, потом использование, использование, использование, ... использование ещё много много раз".

    Если я правильно поняла, вот это вот многократное юзание фичи из памяти вы не исследуете. Просто по той причине, что лямбда такого не может в принципе - она возникает, срабатывает и исчезает сразу же.

    ЗЫ повторюсь, что я не питонистка, и... самой интересно, а как оно на самом деле


    1. Tishka17
      01.01.2022 16:02
      +4

      Лямбда может быть использована много раз, например, когда вы её передаёте дальше в качестве кольэк-функкии или ещё как. Ну и никто не мешает лямбду сохранить в переменную, хотя, конечно, это считается плохим тоном.


      1. Arastas
        01.01.2022 20:54

        Почему сохранения хэндла функции в переменную это плохой тон? А если ее надо несколько раз использовать? Или считается, что тогда надо создавать отдельную функцию? Я в питоне ничего не знаю, но мне интересна мотивация.


        1. Tishka17
          02.01.2022 00:46

          Потому что суть анонимной функции в том, чтобы у нее не было имени (создал и передал куда-то сразу). Если нужно дать функции какое-то имя - есть обычная декларация через def. Заодно оно и в трейсах будет нормально выглядеть.

          Ну и pep8 так рекомендует если моего мнения недостаточно


  1. valis
    01.01.2022 20:00

    Да вы батенька извращенец. У меня и мысли не было о том, что они хоть как-то должны отличатся.


  1. ya_ne_znau
    01.01.2022 20:00
    +1

    Выше написали про это, но я добавлю:

    import timeit
    exec_time = timeit.timeit(func, number=n)

    И так, чтобы не писать boilerplate:

    def timewrap(func):
      def wrapper(number: int):
        return timeit.timeit(func, number=number)
      return wrapper
     
     @timewrap
     def test_whatever():
       pass


  1. BasicWolf
    01.01.2022 20:31
    +1

    Кхм... вы кажется переливаете из пустого в порожнее. Заглянем в документацию:

    It is also possible to create anonymous functions (functions not bound to a name), for immediate use in expressions. This uses lambda expressions, described in section Lambdas. Note that the lambda expression is merely a shorthand for a simplified function definition; ...


    1. kai3341
      02.01.2022 06:02

      Для меня после выныривания из JS обратно в родной Python это перестало быть очевидным -- уже привык, что функция и лямбда (стрелочная функция) совсем ни разу не одно и то же. Д -- деградация


  1. AndreiChernykh1991
    02.01.2022 10:14
    -2

    реально интересная тема!!!!