Подтвердить что ты не робот

Почему str.strip() намного быстрее, чем str.strip('')?

Разделение по белому пространству можно сделать двумя способами: str.strip. Вы можете либо выдать вызов без аргументов, str.strip(), который по умолчанию использует разделитель белого пространства или явно предоставляет аргумент str.strip(' ').

Но почему это так, когда эти функции выполняются по-разному?

Используя примерную строку с преднамеренным количеством пробелов:

s = " " * 100 + 'a' + " " * 100

Тайминги для s.strip() и s.strip(' ') равны соответственно:

%timeit s.strip()
The slowest run took 32.74 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 396 ns per loop

%timeit s.strip(' ')
100000 loops, best of 3: 4.5 µs per loop

strip принимает 396ns, а strip(' ') принимает 4.5 μs, аналогичный сценарий присутствует с rsplit и lsplit при тех же условиях. Кроме того, bytes objects, похоже, тоже будут затронуты.

Тайминги выполнялись для Python 3.5.2, на Python 2.7.1 разница была менее резкой. docs на str.split не указывают ничего полезного, поэтому почему это происходит?

4b9b3361

Ответ 1

В режиме tl; dr:

Это потому, что существуют две функции для двух разных случаев, как это видно в unicode_strip; do_strip и _PyUnicodeXStrip первый выполняется намного быстрее, чем второй.

Функция do_strip для общего случая str.strip(), где аргументов нет, и do_argstrip (который обертывает _PyUnicode_XStrip) для случая, когда вызывается str.strip(arg), то есть аргументы предоставляются.


do_argstrip просто проверяет разделитель, и если он действителен и не равен None (в этом случае он вызывает do_strip), он вызывает _PyUnicode_XStrip.

Оба do_strip и _PyUnicode_XStrip следуют одной и той же логике, используются два счетчика, один равный нулю, а другой - длина строки.

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

Разница заключается в том, как проверять, не совпадает ли текущий символ с разделителем.

Для do_strip:

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

while (i < len) {
    Py_UCS1 ch = data[i];
    if (!_Py_ascii_whitespace[ch])
        break;
    i++;
}
  • Доступ к текущему символу в данных выполняется быстро, путем обращения к базовому массиву: Py_UCS1 ch = data[i];
  • Проверка, является ли символ белым пространством, производится простым индексом массива в массив, называемый _Py_ascii_whitespace[ch].

Итак, короче говоря, он достаточно эффективен.

Если символы не находятся в диапазоне ascii, различия не настолько резкие, но они замедляют общее выполнение:

while (i < len) {
    Py_UCS4 ch = PyUnicode_READ(kind, data, i);
    if (!Py_UNICODE_ISSPACE(ch))
        break;
    i++;
}
  • Доступ осуществляется с помощью Py_UCS4 ch = PyUnicode_READ(kind, data, i);
  • Проверка, является ли символ пробелом, выполняется макросом Py_UNICODE_ISSPACE(ch) (который просто вызывает другой макрос: Py_ISSPACE)

Для _PyUnicodeXStrip:

В этом случае доступ к базовым данным осуществляется, как и в предыдущем случае, с помощью PyUnicode_Read; проверка, с другой стороны, чтобы увидеть, является ли символ белым пространством (или действительно, любым персонажем, который мы предоставили), является немного более сложным.

while (i < len) {
     Py_UCS4 ch = PyUnicode_READ(kind, data, i);
     if (!BLOOM(sepmask, ch))
         break;
     if (PyUnicode_FindChar(sepobj, ch, 0, seplen, 1) < 0)
         break;
     i++;
}

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

Для тех, кто заинтересован, PyUnicode_FindChar после довольно некоторых проверок, в конечном итоге вызовет find_char внутри stringlib, который в случае, когда длина разделителей < 10 будет зацикливаться до тех пор, пока не найдет символ.

Кроме того, рассмотрите дополнительные функции, которые нужно уже вызывать, чтобы попасть сюда.


Что касается lstrip и rstrip, ситуация аналогична. Флаги, для которых существует режим полосания, а именно: RIGHTSTRIP для rstrip, LEFTSTRIP для lstrip и BOTHSTRIP для strip. Логика внутри do_strip и _PyUnicode_XStrip выполняется условно на основе флага.

Ответ 2

По причинам, объясняемым в @Jims, ответ аналогичное поведение находится в bytes объектах:

b = bytes(" " * 100 + "a" + " " * 100, encoding='ascii')

b.strip()      # takes 427ns
b.strip(b' ')  # takes 1.2μs

Для объектов bytearray этого не происходит, функции, выполняющие split в этом случае, схожи для обоих случаев.

Кроме того, в Python 2 то же самое относится в меньшей степени в соответствии с моими таймингами.