Замена пустых строк в строке

Я случайно обнаружил, что в python операция формы

string1.join(string2)

Может быть эквивалентно выражено как

string2.replace('', string1)[len(string1):-len(string1)]

Кроме того, после попытки timeit с несколькими входами различного размера этот странный способ соединения, кажется, более чем в два раза быстрее.

  • Почему метод объединения будет медленнее?
  • Является ли замена пустой строки такой, как безопасная/четко определенная вещь?

Ответ 1

Итак, прежде всего, давайте сломаем, почему это работает.

>>> string1 = "foo"
>>> string2 = "bar"
>>> string1.join(string2)
'bfooafoor'

Это операция поместить string1 между каждым элементом (символом) string2.

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

>>> string2.replace('', string1)
'foobfooafoorfoo'

Таким образом, нарезка из них дает тот же результат, что и str.join():

>>> string2.replace('', string1)[len(string1):-len(string1)]
'bfooafoor'

Очевидно, что это решение намного, гораздо менее читаемо, чем str.join(), и поэтому я всегда рекомендую против него. str.join() также был разработан, чтобы быть эффективным на всех платформах. Замена пустой строки может быть намного менее эффективной для некоторых версий Python (я не знаю, может ли это быть, но это возможно - так же, как повторное конкатенация достаточно быстро в CPython, но это не обязательно в другом месте.)

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

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

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

Эта операция также довольно редка - более распространено иметь последовательность строк для объединения - объединение отдельных символов строки - это не то, что я лично должен был делать часто.

Интересно, что этот x.replace("", y) представляется специальным в источнике Python:

2347 /* Algorithms for different cases of string replacement */
2348
2349 /* len(self)>=1, from="", len(to)>=1, maxcount>=1 */
2350 Py_LOCAL(PyStringObject *)
2351 replace_interleave(PyStringObject *self,
2352 const char *to_s, Py_ssize_t to_len,
2353 Py_ssize_t maxcount)
2354 {
...

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

Ответ 2

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

count = self_len+1;

count -= 1;
Py_MEMCPY(result_s, to_s, to_len);
result_s += to_len;
for (i=0; i<count; i++) {
    *result_s++ = *self_s++;
    Py_MEMCPY(result_s, to_s, to_len);
    result_s += to_len;
}

/* Copy the rest of the original string */
Py_MEMCPY(result_s, self_s, self_len-i);

Метод Join также имеет Loop, но есть области улучшений (я не нашел всех аспектов по причине того, что был закодирован следующим образом) и причины узкого места.

char *sep = PyString_AS_STRING(self);
seq = PySequence_Fast(orig, "");
/* Catenate everything. */
p = PyString_AS_STRING(res);
for (i = 0; i < seqlen; ++i) {
    size_t n;
    item = PySequence_Fast_GET_ITEM(seq, i);
    n = PyString_GET_SIZE(item);
    Py_MEMCPY(p, PyString_AS_STRING(item), n);
    p += n;
    if (i < seqlen - 1) {
        Py_MEMCPY(p, sep, seplen);
        p += seplen;
    }
}

Как вы можете видеть здесь, внутри петли

  • Каждый элемент строки индексируется
  • Определяется размер элемента.
  • Индексированный элемент преобразуется в строку

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

Также сравнивая оба цикла,

Бывший

Заключительная записка

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