Что происходит при изменении строки в Python
Что проверяют
Понимаете ли вы разницу между изменением объекта и перепривязкой имени, и знаете ли, во что это обходится по памяти и скорости. Следом обычно спрашивают, как правильно собрать строку из тысяч кусков.
Как отвечать
Строка str неизменяема. Попытка s[0] = "J" падает с TypeError: 'str' object does not support item assignment. Всё, что выглядит как изменение (+, replace(), upper(), срезы, f-строки), создаёт новый объект, а исходный остаётся прежним.
Запись s = s.upper() не меняет строку, а привязывает имя s к новому объекту. Это видно по id(s), и это же объясняет, почему другие ссылки на старую строку, например в списке или в аргументе функции, ничего не заметят. Старый объект CPython освобождает, когда счётчик ссылок падает до нуля.
Про производительность стоит сказать конкретно:
- сборка в цикле:
s += partв теории даёт квадратичное время, так как каждый раз копируется всё накопленное. Правильный путь это"".join(parts)илиio.StringIO; - оптимизация CPython: интерпретатор умеет дописывать строку на месте, если на неё нет других ссылок. Это деталь реализации, в PyPy и при лишней ссылке она не срабатывает, и PEP 8 прямо советует на неё не полагаться;
- хранение: по PEP 393 строка занимает 1, 2 или 4 байта на символ в зависимости от самого «широкого» символа в ней. Один эмодзи в длинном ASCII-тексте увеличивает его вес в памяти примерно вчетверо;
- интернирование: короткие строки, похожие на идентификаторы, CPython может хранить в одном экземпляре, вручную это делается через
sys.intern().
Если нужна именно изменяемая последовательность, берут list символов, bytearray для байтов или io.StringIO.
Где спотыкаются
Говорят, что replace() меняет строку. Метод возвращает новую, и если результат не присвоить, работа просто теряется.
Сравнивают строки через is. Из-за интернирования это иногда срабатывает в REPL и ломается на данных из файла или сети, сравнивать надо через ==.
Уверенно утверждают, что += в цикле всегда медленный или всегда быстрый. Правильный ответ: зависит от реализации, поэтому в рабочем коде используют join.