Почему создание набора из фильтра происходит намного быстрее, чем создание списка или кортежа?

#python #python-3.x #python-internals

#python #python-3.x #python-внутренности

Вопрос:

Я работаю filter с interable и хочу сохранить результат в последовательности (мне нужна последовательность, чтобы я мог использовать random.choice ее). Я заметил, что создание набора из объекта filter намного быстрее, чем создание списка или кортежа. Почему это? Сначала я подумал, что тип фильтра является подтипом set , что могло бы объяснить это, но filter функция фактически идентична выражению генератора, поэтому она не может быть внутренним набором.

Я выполнил следующий тест, чтобы проверить скорость:

 import time

def test ( n, seq ):
    for method in ( set, list, tuple ):
        t = time.time()
        for i in range( n ):
            method( seq )
        print( method.__name__, ( time.time() - t ) )

someFilter = filter( lambda x: x % 3 == 0, range( 1000 ) )
test( 10000000, someFilter )
  

И результаты ясно говорили в пользу использования набора:

 set 1.9240000247955322
list 8.82200002670288
tuple 7.031999826431274
  

Итак, почему создание набора из фильтра происходит намного быстрее? Разве обычно это не должно занимать столько же времени, сколько создание набора из последовательности, где каждый элемент должен быть хэширован? Или это каким-то образом улучшается благодаря представлению внутреннего фильтра?

Для сравнения, при выполнении теста для range выражения set требуется примерно в два раза больше времени, чем для list и tuple (которые оба почти идентичны по скорости).

Редактировать:

Ответ Sven абсолютно правильный, но для полноты картины обновленный тест, который будет выполняться на реальном фильтре:

 import time

def testFilter ( n, test, rangeSize ):
    for method in ( set, list, tuple ):
        t = time.time()
        for i in range( n ):
            method( filter( test, range( rangeSize ) ) )
        print( method.__name__, ( time.time() - t ) )

testFilter( 100000, lambda x: x % 3 == 0, 1000 )
  

Результат на самом деле показывает, что имеет больше смысла, когда list и tuple оба являются самыми быстрыми, хотя set на самом деле не медленнее, так что не будет иметь никакого значения, что использовать:

 set 27.868000030517578
list 27.131999969482422
tuple 27.138000011444092
  

Комментарии:

1. Использовать timeit.timeit было бы проще.

2. @ChrisMorgan Да, я знаю, но я все время забываю, как это использовать, поэтому я всегда в конечном итоге делаю такие маленькие тестеры сам ^^

3. @Chris: Я не согласен. timeit может быть проще для одноразовых тестов, состоящих из одной строки, выполняемых в командной строке. Но если вы собираетесь написать небольшую программу (возможно, вы еще не точно знаете, что собираетесь тестировать, возможно, вы захотите ее изменить, добавить к ней или повторно использовать), то сделать это, как показано в вопросе, по крайней мере, так же просто, как timeit а может быть, и еще проще. Кроме того, совершенно интуитивно использовать показания часов для измерения времени, тогда как на самом деле мне приходится обращаться к документации, чтобы вспомнить, как использовать timeit , особенно если задействована настройка.

4. Упс. Думаю, мне нужно чаще обновляться.

Ответ №1:

filter() возвращает итератор в Python 3, и этот итератор будет использован при первом запуске внутреннего цикла for. После этого вы только измеряете скорость конструктора — вот почему вам приходится повторять это так часто, чтобы это заняло хотя бы немного времени.

Итак, кажется, что конструктор set() является самым быстрым при работе с пустым итератором.

Комментарии:

1. Ой, да, это имеет смысл. Что за глупая ошибка! Большое спасибо, что указали на это! 🙂

2. Относительные тайминги в 2.7, где фильтр не является генератором, установлены = 1.5767, список = 0.1468, кортеж = 0.1556

3. filter на самом деле уже возвращает список в Python 2, так что это объясняет скорость 😉

Ответ №2:

Когда тайминги предполагают нелогичные результаты, часто виноват сам набор таймингов 😉

Попробуйте использовать модуль timeit, который поможет вам избежать распространенных ошибок синхронизации. В частности, вы хотите запускать новую настройку для каждого теста и выполнять только тело цикла, а не тело плюс тестовый код.

В этом случае это, по крайней мере, сделало бы временные интервалы сопоставимыми (все они использовали бы новый итератор, возвращаемый версией *filter в Python 3) и показало бы невероятно быстрые временные интервалы (потому что только method(iterator) код был бы синхронизирован в цикле).

FWIW, pypy будет еще сложнее по времени, потому что чрезмерно простые циклы полностью оптимизируются.

[Отредактированный ответ на вопрос] Ваши новые тайминги сопоставимы (хорошее улучшение), но результаты по-прежнему показывают сочетание времени настройки и времени цикла, что затрудняет обнаружение различий, которые могут быть существенными. Вы должны ожидать, что списки и кортежи превзойдут наборы, потому что наборам приходится выполнять больше работы (хэшировать каждый из входных данных, а не просто сохранять их).

Комментарии:

1. Да, вы правы, но с Python вы никогда не знаете, действительно ли это нелогично или просто какая-то потрясающая скрытая языковая функция : D

2. Думаю, мне действительно придется потратить еще немного времени на модуль timeit, чтобы в следующий раз наконец-то уложить это в голове. Спасибо за ваши пояснения!