2016-01-16 2 views
5

Рассмотрите проблему извлечения алфавитов из огромной строки.Соединение строк. Понимание генератора или списка?

Один из способов сделать это

''.join([c for c in hugestring if c.isalpha()]) 

Механизм понятен: Список понимание формирует список символов. Метод join знает, сколько символов ему нужно объединить, обратившись к длине списка.

Другой способ сделать это

''.join(c for c in hugestring if c.isalpha()) 

Здесь результаты генератора постижение в генераторе. Метод соединения не знает, сколько символов он собирается присоединиться, потому что генератор не обладает атрибутом len. Таким образом, этот способ соединения должен быть медленнее, чем метод понимания списка.

Но тестирование на питоне показывает, что оно не медленнее. Почему это так? Может ли кто-нибудь объяснить, как соединение работает на генераторе.

Чтобы был ясен:

sum(j for j in range(100)) 

не нужно иметь какие-либо знания 100, поскольку он может следить за нарастающий итог. Он может получить доступ к следующему элементу, используя следующий метод для генератора, а затем добавить к суммарной сумме. Однако, поскольку строки неизменяемы, объединение строк кумулятивно создало бы новую строку на каждой итерации. Так что это займет много времени.

ответ

10

Когда вы вызываете str.join(gen), где gen является генератором, Python выполняет эквивалент list(gen), прежде чем переходить к рассмотрению длины результирующей последовательности.

В частности, если вы look at the code implementing str.join in CPython, вы увидите этот призыв:

fseq = PySequence_Fast(seq, "can only join an iterable"); 

Вызов PySequence_Fast преобразует seq аргумента в список, если он не был список или кортеж уже.

Итак, две версии вашего вызова обрабатываются почти одинаково. В понимании списка вы сами создаете список и передаете его в join. В версии выражения генератора передаваемый объект генератора превращается в list в начале join, а остальная часть кода работает одинаково для обеих версий.

+0

Значит, разница в скорости OP-уведомления должна быть чисто косвенной, не так ли? –

+0

@ Ev.Kounis: Вопросник сказал, что две версии были схожи по скорости («** не ** медленнее»), что имеет смысл, если они измеряют время, затраченное на «join», и время, затраченное на понимание списка вместе. Если вы измеряли только «join», версия выражения генератора была бы медленнее, поскольку она должна была сбрасывать весь генератор в список, прежде чем выполнять объединение строк. Это займет примерно столько же времени, сколько и построение понимания списка в другой версии. – Blckknght

1

join() не нужно реализовывать как последовательное присоединение элементов последовательности к более длинной и более длинной скопированной строке (что было бы очень медленным для длинных последовательностей); ему просто нужно произвести тот же результат. Таким образом, join(), вероятно, просто добавляет символы в некоторый буфер внутренней памяти и создает из него строку в конце. С другой стороны, конструкцию понимания списка нужно сначала построить список (путем перемещения генератора hugestring), и только тогда пусть join() начнет свою работу.

Кроме того, я сомневаюсь, что join() смотрит на длину списка, так как он не может знать, что каждый элемент является единственным символом (в большинстве случаев его не будет) - он, вероятно, просто получает генератор из списка ,

+2

Код ссылочного интерпретатора C-layer полный (но частный) API '_PyUnicodeWriter' для этой цели (и другие подобные« строковые строки поэтапных »случаев). Сравните с классом Java StringBuilder. – ShadowRanger

+1

Тем не менее, похоже, что @Blckknight верен; он конвертирует вход в «список» внутри, если он еще не является «списком» или «кортежем». Похоже, что тогда выполняется прекомплекс, чтобы рассчитать длину конечного значения, чтобы предварительно распределить столько, сколько ему нужно, вместо того, чтобы использовать '_PyUnicodeWriter' вообще. – ShadowRanger

1

По крайней мере, на моей машине понимание списка выполняется быстрее для случая, который я тестировал, вероятно, из-за того, что ''.join способен оптимизировать распределение памяти. Это, вероятно, зависит только от конкретного примера, который вы тестируете (например, если условие вы тестируете происходит реже, цена CPython платит не зная длину загодя может быть меньше):

In [18]: s = ''.join(np.random.choice(list(string.printable), 1000000)) 

In [19]: %timeit ''.join(c for c in s if c.isalpha()) 
10 loops, best of 3: 69.1 ms per loop 

In [20]: %timeit ''.join([c for c in s if c.isalpha()]) 
10 loops, best of 3: 61.8 ms per loop 
+1

Это артефакт переосмысления списков, которые являются гипероптимизированными (они строят «список» напрямую, где выражение генератора просто «дает значения', которые должны потребляться с использованием протокола итератора общего назначения), а не что-то конкретное, как «». присоединиться'. Запустите тот же тест, но замените '' '.join' на 'list' (вы можете полностью опустить его во втором случае, где он является избыточным). Конструктор 'list' вокруг выражения генератора значительно медленнее, и для ввода этого большого значения он явно не имеет ничего общего с затратами на поиск или функцию, связанными с' list'. – ShadowRanger

Смежные вопросы