Deep Engineering
Средний·Опубликовано·3.11 · 3.12 · 3.13 · 3.14·30 МИН

Строки и форматирование: неизменяемость, срезы, f-строки и цена склейки

Вопросы про строки проверяют, понимаете ли вы, что строка неизменяема и что из этого следует. Прогон отвечает: s[0] = 'П' даёт TypeError; после t = s; s += '!' имя t по-прежнему видит старую строку; границы среза урезаются до длины строки — 'Python'[10:20] пустая, 'Python'[:100] целая, а индекс за пределами — IndexError; f'{2.5:.0f}' печатает 2, потому что округление идёт к чётному; 'привет' — шесть кодовых точек и двенадцать байт; а быстрый путь CPython для s += x срабатывает не всегда: его выключили лишняя ссылка, посчитанный хеш, подкласс str, символ вне ASCII и — на 3.13 и 3.14 — второй оператор на той же строке кода. Собирать строку надёжно через join.

Полное техническое изложение

TL;DR

Строка в Python — неизменяемая последовательность кодовых точек Юникода (кодовая точка — один номер в таблице Юникода; обычно это одна буква, но не всегда). Заменить символ нельзя: s[0] = 'П' даёт TypeError. Методы — upper, replace, strip — возвращают результат, а исходная строка не меняется. А s += '!' связывает s с новым значением: имя t, взятое до этого, по-прежнему видит старое.

Отсюда главное следствие: собирать строку из многих кусков — "".join(parts). Документация называет повторную склейку квадратичной. У CPython есть быстрый путь, который иногда дописывает строку на месте, но условия у него — деталь реализации, и в прогоне его выключало многое: вторая ссылка на строку (на двадцати тысячах кусков склейка стала в 83 раза дороже на кусок, чем join), посчитанный хеш, подкласс str, символ вне ASCII, а на 3.13 и 3.14 — даже второй оператор на той же строке кода.

Дальше — то, что отличает понимание от пересказа. Срез не падает: его границы урезаются до длины строки — 'Python'[10:20] пустая, а 'Python'[:100] целая. f'{2.5:.0f}' печатает 2: округление к чётному. Длина строки — в кодовых точках, а в UTF-8 — в байтах: 'привет' — шесть точек и двенадцать байт, а буква é, записанная как 'e\u0301', — две точки. А f-строки менялись: кавычки того же вида внутри выражения можно ставить только с 3.12, а с 3.14 есть t-строки.

Прогоны — CPython 3.11.15, 3.12.3, 3.13.7 и 3.14.7 на Linux x86_64; сверено 14.10.2026. Вышедший к этой дате Python 3.15 урок не запускал.

Порог входа
Перед уроком достаточно понимать
  • как создать строку и напечатать её;
  • что такое индекс элемента последовательности;
  • что имя в Python — ссылка на объект, и два имени могут ссылаться на один объект; подробно — в уроке «is против ==»;
  • что значит O(n²) — урок «Сложность операций»; для раздела «Глубже» — что такое хеш объекта: урок «Встроенные коллекции».
Заранее знать не нужно
  • неизменяемость строки, срезы, split, join, find против index;
  • спецификации формата, отладочное =, округление к чётному;
  • str и bytes, цена += в цикле, f-строки в 3.12 и t-строки в 3.14.

База: строка — неизменяемая последовательность

Строку индексируют, режут и перебирают, как список, но изменить не могут. Документация говорит о таких типах прямо:

An object of an immutable sequence type cannot change once it is created.

перевод

Объект неизменяемой последовательности не может измениться после создания.

— Модель данных — неизменяемые последовательности

Ответ уровня Junior на «что такое строка в Python»: неизменяемая последовательность символов Юникода — точнее, кодовых точек; методы возвращают результат и не меняют исходную строку; форматируют её f-строками. Это верно, и за этим ответом идут уточнения: что тогда делает +=, почему склейка в цикле может быть медленной, чем find отличается от index и сколько байт занимает строка. Механизмы ниже отвечают на них прогоном.

Как читать блоки кода: блок с пометкой «полный файл» копируется как есть; блок без языка — дословный вывод прогона из bench/strfmt/runs/; если показана только часть раздела, это сказано рядом.

Сначала — полный короткий пример: его можно скопировать в пустой каталог и запустить. Полный файл bench/strfmt/example.py, запуск — python3 bench/strfmt/example.py:

PYTHON
import unicodedata
 
s = "cafe"
t = s
s += "\u0301"  # комбинирующий знак ударения: вместе с e рисуется как é
print(ascii(s), t, s is t)
print(len(s), len(s.encode("utf-8")), len(unicodedata.normalize("NFC", s)))
print(repr("Python"[10:20]), repr("Python"[:100]))
print("-".join(["a", "b", "c"]))

Вывод, одинаковый на CPython 3.11–3.14:

'cafe\u0301' cafe False
5 6 4
'' 'Python'
a-b-c

t видит старое значение; в s пять кодовых точек и шесть байт UTF-8, хотя на экране четыре буквы, — после нормализации NFC (склейки буквы и знака в одну точку) их четыре; срез за пределами строки пуст, а частично выходящий урезается; join склеивает куски через разделитель. Каждая строка разобрана ниже.

Механизм 1: неизменяемость и +=

контракт языкаНеизменяемость — модель данных; вывод — прогон bench/strfmt/basics.py, разделы 1 и 7, одинаковые на CPython 3.11–3.14 (Linux x86_64). То, что метод вернул тот же объект, — наблюдение реализации CPython, не контракт.
1) неизменяемость
   s[0] = 'П'          -> TypeError: 'str' object does not support item assignment
   s.upper()           -> 'ПРИВЕТ' | s -> 'привет'
   t = s; s += '!'     -> s = 'привет!' | t = 'привет' | s is t: False

Три строки — три следствия:

  • Символ не заменить. Чтобы «поменять первую букву», строят новую строку: 'П' + s[1:].
  • Методы возвращают результат, исходная строка не меняется. s.upper() не трогает s; результат надо сохранить: s = s.upper().
  • s += '!' — новое значение и перевязка имени. t, взятое до этого, указывает на старую строку. Так же, как с присваиванием любого неизменяемого объекта. Оговорка: если на старую строку никто, кроме s, не ссылается, CPython может дописать её на месте (раздел «Глубже»), — но заметить это изменение уже некому.

Неизменяемость обещает, что не изменится значение, которое кто-то видит. Она не обещает, что каждый метод создаст новый объект. Раздел 7 того же прогона (добавлен 14.10.2026), здесь p = 'Python':

7) границы среза, кодовые точки, тот же объект
   p.strip() is p                                -> True
   p.replace('z', 'y') is p                      -> True
   p[:100] is p                                  -> True
   p.upper() is p.upper()                        -> False

Когда результат совпал с исходной строкой, CPython 3.11–3.14 вернул тот же объект; когда не совпал — новый. Это наблюдение реализации, а не правило языка: полагаться на is для строк нельзя.

Механизм 2: срезы и индексы

контракт языкаПравила индексов и срезов — документация последовательностей; вывод — прогон bench/strfmt/basics.py, разделы 2 и 7, одинаковые на CPython 3.11–3.14.
2) срезы
   p[0]      -> 'P'
   p[-1]     -> 'n'
   p[1:4]    -> 'yth'
   p[:2]     -> 'Py'
   p[-3:]    -> 'hon'
   p[::2]    -> 'Pto'
   p[::-1]   -> 'nohtyP'
   p[10:20]  -> ''
   p[10]     -> IndexError: string index out of range

Правила, которые проверяют на собеседовании:

  • Правая граница не входит. p[1:4] — символы 1, 2 и 3; длина среза — 4 − 1.
  • Отрицательный индекс считается с конца. p[-1] — последний символ, p[-3:] — три последних.
  • p[::-1] возвращает развёрнутую строку, исходная не меняется. Разворачиваются кодовые точки, а не видимые буквы — для текста с комбинирующими знаками это ломает текст (механизм 5).
  • Срез прощает, индекс — нет. Границы среза урезаются до длины строки: p[10:20] целиком за пределами и даёт пустую строку, а p[10] — IndexError. Частично выходящий срез не пуст:
7) границы среза, кодовые точки, тот же объект
   p[:100]   -> 'Python'
   p[4:100]  -> 'on'
   p[-100:2] -> 'Py'

Картинка выше — учебный ASCII-случай: в 'Python' каждая клетка — и кодовая точка, и видимая буква. Для произвольного текста это не так.

Механизм 3: методы, о которых спрашивают

контракт языкаВывод — прогон bench/strfmt/basics.py, раздел 3, одинаковый на CPython 3.11–3.14.
3) методы
   'a,b,,c'.split(',')               -> ['a', 'b', '', 'c']
   '  a  b '.split()                 -> ['a', 'b']
   '-'.join(['a', 'b', 'c'])         -> 'a-b-c'
   '-'.join([1, 2])                  -> TypeError: sequence item 0: expected str instance, int found
   '  hi  '.strip()                  -> 'hi'
   'abc'.find('z')                   -> -1
   'abc'.index('z')                  -> ValueError: substring not found
   'hello'.replace('l', 'L', 1)      -> 'heLlo'
   'hello world'.title()             -> 'Hello World'
   '42'.isdigit(), '-42'.isdigit()   -> (True, False)

Четыре места, где ответы расходятся с ожиданием:

  • split(',') и split() — разные операции. С разделителем пустые куски остаются: 'a,b,,c' даёт четыре элемента. Без аргумента строка делится по пробельным символам, подряд идущие считаются одним разделителем, и пустых кусков нет.
  • join — метод разделителя, и принимает только строки. '-'.join([1, 2]) падает; числа сначала превращают в строки: '-'.join(map(str, nums)).
  • find против index. Не нашли — find возвращает −1, index бросает ValueError. Проверка if s.find(x): — ошибка: −1 истинно, а 0 — ложно.
  • isdigit() не проверяет «это число». Для '-42' — False. Надёжнее попробовать int(s) и поймать ValueError.

Механизм 4: форматирование

контракт языкаСпецификации формата и правило округления float — документация; история f-строк — справочник языка 3.14. Вывод — прогон bench/strfmt/basics.py, разделы 4 и 6; раздел 4 одинаков на CPython 3.11–3.14, раздел 6 различается по версиям.
4) форматирование
   f'{x:.2f}'          -> '3.14'
   f'{n:,}'            -> '1,234,567'
   f'{n:_}'            -> '1_234_567'
   f'{name:>6}|'       -> '   Аня|'
   f'{name:*^9}'       -> '***Аня***'
   f'{name!r}'         -> "'Аня'"
   f'{x=}'             -> 'x=3.14159'
   f'{x=:.2f}'         -> 'x=3.14'
   f'{0.25:.0%}'       -> '25%'
   '{} и {}'.format(1, 2) -> '1 и 2'
   '%s: %d' % ('n', 5) -> 'n: 5'
   f'{0.5:.0f} {1.5:.0f} {2.5:.0f}' -> '0 2 2'
   round(2.5), round(3.5) -> 2 4
   Decimal('2.5').quantize(Decimal('1'), rounding=ROUND_HALF_UP) -> 3
   f'{2.675:.2f}', round(2.675, 2) -> '2.67' 2.67
   Decimal(2.675) -> 2.67499999999999982236431605997495353221893310546875

После двоеточия в f-строке — спецификация формата: точность (.2f), разделитель тысяч (, или _), выравнивание и заполнение (>6, *^9), проценты (.0%). !r подставляет repr вместо str — с кавычками. f'{x=}' печатает и имя, и значение — удобно для отладки, добавлено в 3.8. str.format понимает ту же спецификацию после двоеточия. У % свой, более узкий синтаксис в духе printf: '%s: %d' работает, а разделителя тысяч, заполнения и выравнивания по центру в нём нет. В f-строке выражение стоит прямо там, где окажется его значение.

Последние строки — ловушка. f'{2.5:.0f}' — это 2, а не 3, и round(2.5) — тоже 2: число ровно посередине округляется к чётному, и документация прямо говорит, что форматирование float округляет так же, как round(). «Ровно посередине» — в двоичном представлении: 2.5 представимо точно, а 2.675 — нет. Две последние строки прогона показывают, что за этим литералом хранится 2.67499…, поэтому и f'{2.675:.2f}', и round(2.675, 2) дают 2.67 — правило к чётному тут ни при чём. Это не ошибка, а правило; если нужно «школьное» округление половины вверх, его делают модулем decimal с явным режимом ROUND_HALF_UP.

Версии. В 3.12 с f-строк сняли часть ограничений. Внутри выражения теперь можно ставить кавычки того же вида, что и снаружи:

6) версии: что компилируется
   f"{d["k"]}"   -> '1'
   f"{d['k']}"   -> '1'
   t"x={1 + 1}" -> SyntaxError: invalid syntax

Это вывод 3.13. На 3.11 первая строка — SyntaxError: f-string: unmatched '[': код, написанный под 3.12, на 3.11 не скомпилируется. А на 3.14 третья строка компилируется — это t-строка, шаблонный литерал, — и даёт объект Template, а не строку: выражение внутри вычисляется сразу — на 3.14 прогон печатает Template, значение выражения уже вычислено: 2, — но в текст его собирает не язык, а код, который обрабатывает шаблон.

Механизм 5: str и bytes

контракт языкаВывод — прогон bench/strfmt/basics.py, разделы 5 и 7, одинаковые на CPython 3.11–3.14. Про UTF-8 по умолчанию в 3.15 — цитата release notes, не прогон.
5) str и bytes
   'hello'    len = 5, в UTF-8 байт = 5
   'привет'   len = 6, в UTF-8 байт = 12
   '👍'        len = 1, в UTF-8 байт = 4
   'привет'.encode()[0]  -> 208 — байт, это int, а не символ
   b.decode()            -> 'привет'
   'привет'.encode('ascii') -> UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)
   'a' + b'b'            -> TypeError: can only concatenate str (not "bytes") to str

str — текст из кодовых точек Юникода, bytes — последовательность байтов. Между ними переходят явно: encode превращает текст в байты в выбранной кодировке, decode — обратно. Отсюда три следствия: len строки считает кодовые точки, а не байты, — кириллическая буква в UTF-8 занимает два байта, '👍' — четыре; элемент bytes — число, а не символ; смешивать str и bytes в одной операции нельзя.

Кодовая точка — не то же, что видимая буква. Раздел 7 прогона; входы с не-ASCII символами записаны escape-последовательностями, FAMILY — эмодзи «семья» из четырёх человек:

7) границы среза, кодовые точки, тот же объект
   FAMILY = '\U0001f468\u200d\U0001f469\u200d\U0001f467\u200d\U0001f466'
   len('\u00e9')                                 -> 1
   len('e\u0301')                                -> 2
   len(unicodedata.normalize('NFC', 'e\u0301'))  -> 1
   len('\U0001F44D\U0001F3FD')                   -> 2
   len(FAMILY)                                   -> 7
   ascii('cafe\u0301'[::-1])                     -> '\u0301efac'

Буква é бывает одной точкой ('\u00e9') и двумя — e и комбинирующий знак ударения U+0301; на экране они одинаковы, а len разный. unicodedata.normalize('NFC', …) приводит такую пару к одной точке. Палец вверх с оттенком кожи — две точки, «семья» — семь: четыре человека и три невидимых соединителя U+200D. А [::-1] переставляет точки: у развёрнутого 'cafe\u0301' знак ударения оказался первым и больше не стоит над e. Поэтому len — не число видимых символов, а [::-1] — не разворот произвольного текста.

В сеть и в файл в двоичном режиме уходят байты, а у текстового файла кодировку стоит называть явно: open(path, encoding="utf-8"). Без неё на 3.11–3.14, как сказано в open.__doc__, The default encoding is platform dependent (кодировка по умолчанию зависит от платформы). В Python 3.15, по release notes, Python now uses UTF-8 as the default encoding, independent of the system's environment (Python теперь по умолчанию использует UTF-8 независимо от окружения системы); прежнее поведение возвращают PYTHONUTF8=0 или -X utf8=0. Урок на 3.15 не запускался. Явная кодировка от этого не устаревает: тот же код работает и на версиях до 3.15.

Глубже: цена склейки и быстрый путь CPython

наблюдение замераbench/strfmt/cost.py, раздел 1, и bench/strfmt/fastpath.py: медианы, CPython 3.13.7 на Linux x86_64, одна машина (2 ядра); 3.11.15, 3.12.3 и 3.14.7 — в записях с суффиксом версии. Форма кода для быстрого пути — Python/bytecodes.c. Это наблюдения одной конфигурации: на другой машине и версии числа и даже набор условий будут другими — мерить у себя.

Документация предупреждает прямо:

Concatenating immutable sequences always results in a new object. This means that building up a sequence by repeated concatenation will have a quadratic runtime cost in the total sequence length.

перевод

Склейка неизменяемых последовательностей всегда даёт новый объект. Поэтому построение последовательности повторной склейкой обходится во время, квадратичное от её итоговой длины.

— Встроенные типы — операции над последовательностями, примечание 6

Замер: n кусков по десять символов, время на один кусок. У линейной склейки оно от n не зависит; если оно растёт вместе с n — это признак квадратичной:

1) склейка n кусков по десять символов, нс на кусок, медианы 7 кругов
   форма                               2000     20000     ×10
   "".join(parts)                      49.8      35.5     0.7
   s += x                              82.9      52.5     0.6
   s += x, есть вторая ссылка         215.7    2942.0    13.6
   io.StringIO                         42.0      49.4     1.2

На этих размерах у join и io.StringIO время на кусок не росло вместе с n — как и обещает документация. Но и у обычного s += x в функции оно не росло. Причина — быстрый путь в интерпретаторе CPython: когда можно, он дописывает строку на месте, а не копирует. В исходниках у этого пути названа цель: This attempts to avoid quadratic behavior when one neglects to use str.join() (Это попытка избежать квадратичного поведения, когда забыли применить str.join()).

Неизменяемость этому не противоречит. Она обещает, что не изменится значение, которое кто-то видит; дописать на месте можно только строку, которую, кроме накопителя, никто не видит. Переиспользовать память — право реализации.

Стоит появиться второй ссылке на ту же строку — в замере это одно лишнее присваивание alias = s в теле цикла, — и дописывать на месте нельзя: каждый += копирует всё накопленное. Время на кусок выросло в 13,6 раза при ×10 кусков (на 3.14.7 — в 9,9), на двадцати тысячах это в 83 раза дороже join.

Условий у быстрого пути больше двух. Комментарий в Python/bytecodes.c называет форму кода — результат += записывается в ту же локальную переменную — и число ссылок. Отдельный прогон bench/strfmt/fastpath.py показывает, что этого мало. Один шаг s += x на строке в миллион символов: доля микросекунды — дописали на месте, сотни микросекунд — скопировали миллион символов (вывод 3.13.7, фрагмент):

2) один шаг s += x на строке в 1 000 000 символов, мкс, медиана 25 шагов
   ничего лишнего                         0.1
   есть вторая ссылка                   288.4
   перед шагом посчитан hash(s)         275.9
   s = sys.intern(s) перед шагом        253.3
   s — экземпляр подкласса str          268.6
   ASCII-строка += ASCII-кусок            0.3
   ASCII-строка += кусок с 'é'          108.5
   строка из 'é' += ASCII-кусок           0.1

Копию дали: вторая ссылка; уже вызванный для строки hash (его вызывает, например, использование строки ключом словаря); sys.intern — внесение строки в общую таблицу интернированных, то есть хранимых в одном экземпляре, строк (хеш при этом тоже считается, так что эти два случая прогон не разделяет); подкласс str; ASCII-строка, к которой дописали символ вне ASCII, — у неё меняется внутреннее представление. Картина одна на 3.11–3.14.

А форма кода зависит от версии. Тот же цикл, записанный в одну строку через ;, на 3.13.7 стал квадратичным:

1) склейка n кусков по десять символов, нс на кусок, медианы 7 кругов
   форма                                 2000     20000     ×10
   s += x                                37.1      36.3     1.0
   s += x; k += 1  (одна строка)        184.1    5942.0    32.3
   s += x и k += 1 (две строки)          50.5      53.6     1.1
3) опкод += после прогрева и следующая за ним инструкция
   own_line   BINARY_OP_INPLACE_ADD_UNICODE    -> STORE_FAST
   one_line   BINARY_OP_ADD_UNICODE            -> STORE_FAST_LOAD_FAST
   two_lines  BINARY_OP_INPLACE_ADD_UNICODE    -> STORE_FAST

Специализация, дописывающая на месте, требует, чтобы сразу за += шла запись в локальную переменную — в Python/bytecodes.c (строка 540 на теге v3.13.7) это assert(next_instr->op.code == STORE_FAST);. На одной строке компилятор 3.13 склеил запись в s и чтение k в одну инструкцию STORE_FAST_LOAD_FAST, и интерпретатор выбрал обычное сложение строк. На 3.14.7 — то же (×35,5), а на 3.11.15 и 3.12.3 запись в одну строку линейна (×1,0 и ×1,1). Накопитель в атрибуте объекта или в глобальной переменной тоже не проходит условие о форме кода — такие случаи измерены в статье «Строки изнутри».

Отсюда ответ на собеседовании: собирать строку из многих кусков — "".join(parts) или io.StringIO; так советует документация, и это не зависит ни от версии, ни от формы кода. Документация описывает повторную склейку как квадратичную; CPython часто дописывает строку на месте, но полный набор условий — деталь конкретной версии, и опираться на него нельзя. Как проверить быстрый путь на своём коде, разобрано в той же статье.

Как отвечать на собеседовании

Короткий ответ: строка неизменяема — методы и += возвращают результат, а исходная строка не меняется. Срезы s[i:j:k] не включают правую границу, отрицательные индексы считаются с конца, границы среза урезаются до длины строки — поэтому срез целиком за её пределами пуст. Форматируют f-строками со спецификацией после двоеточия. Строку из многих кусков собирают через "".join(...): документация называет повторную склейку квадратичной, а быстрый путь CPython — условная оптимизация. str — кодовые точки Юникода, bytes — байты, между ними — encode и decode.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

На «почему += в цикле медленный, если у меня он быстрый». «Документация обещает только, что склейка даёт новый объект, и называет повторную склейку квадратичной. CPython умеет дописывать строку на месте, но условия — деталь реализации: в замере на 3.13 быстрый путь выключали лишняя ссылка, посчитанный хеш, подкласс str, символ вне ASCII и даже s += x; k += 1 в одну строку. Поэтому join — на своей машине и версии это стоит мерить, а не угадывать».

На «как развернуть строку». «s[::-1] — срез с шагом −1, возвращает развёрнутую строку. Он переставляет кодовые точки, поэтому текст с комбинирующими знаками или составными эмодзи так не развернуть».

На «чем find отличается от index». «Не нашли — find возвращает −1, index бросает ValueError. И −1 истинно, поэтому if s.find(x): — ошибка».

На «что напечатает f'{2.5:.0f}'». «2: число ровно посередине округляется к чётному, как и round(2.5)».

Дальше спросят

Спросят дальше

Чем f-строка лучше str.format и %?

Короткий ответ

Читаемостью: выражение стоит прямо в том месте, где его значение попадёт в текст. Спецификация после двоеточия у f-строки и str.format одна и та же; у % свой, более узкий синтаксис. По скорости в замере f-строка обогнала str.format и % на обеих версиях. Ниже — вывод 3.13.7; на 3.14.7 сложение через str() оказалось быстрее f-строки — 131,6 против 152,2 нс:

2) одно форматирование: имя и число, нс, медианы 7 кругов
   f-строка         167.1
   str.format       355.9
   оператор %       271.5
   + и str()        190.5
Спросят дальше

Почему len('привет') — 6, а файл с этим словом весит 12 байт?

Короткий ответ

len строки считает кодовые точки. В файл пишутся байты в кодировке — если файл в UTF-8, кириллическая буква занимает два байта. В прогоне 'привет' — шесть точек и двенадцать байт, '👍' — одна точка и четыре байта.

Спросят дальше

Как изменить один символ в строке?

Короткий ответ

Никак — только построить новую строку: s[:i] + 'X' + s[i + 1:]. Если символов меняется много, удобнее превратить строку в список символов, поменять и собрать обратно через ''.join(chars).

Частые заблуждения

Утверждение

s += '!' меняет строку на месте

На самом деле

Связывает имя с новой строкой. После t = s; s += '!' имя t видит старое значение, s is t — False.

Утверждение

Срез за пределами строки бросает IndexError

На самом деле

Границы среза урезаются до длины строки: p[10:20] даёт пустую строку, p[:100] — всю. IndexError бросает только индекс: p[10].

Утверждение

f'{2.5:.0f}' печатает 3

На самом деле

Печатает 2: значение ровно посередине округляется к чётному. round(2.5) — тоже 2, а round(3.5) — 4.

Утверждение

Склейка через += в цикле всегда квадратична

На самом деле

Документация называет её квадратичной, но CPython при ряде условий дописывает строку на месте — тогда на размерах замера время на кусок не росло. Условия — деталь версии: лишняя ссылка дала ×13,6 на кусок при ×10 кусков, а на 3.13 и 3.14 квадрат возвращала даже запись s += x; k += 1 в одну строку. Надёжный путь — join.

Утверждение

len строки — это её размер в байтах или число видимых букв

На самом деле

Длина в кодовых точках. 'привет' — 6 точек и 12 байт в UTF-8, а буква é, записанная как 'e\u0301', — 2 точки.

История версий

ВерсияИзменениеЧто это значит для кода
3.8В f-строках появился отладочный спецификатор: f'{x=}' печатает имя и значение.
3.11Опорная точка урока. Внутри f-строки нельзя ставить кавычки того же вида, что снаружи: f"{d["k"]}" — SyntaxError: f-string: unmatched '['.
3.12С выражений в f-строках сняли часть ограничений — разрешены вложенные кавычки того же вида, комментарии и обратная косая черта. Код с такими f-строками на 3.11 не компилируется.
3.13Наблюдение прогона, не контракт: s += x; k += 1 в одну строку выключает быстрый путь склейки — ×32,3 на кусок при ×10 кусков; на 3.11 и 3.12 та же запись линейна. На 3.14.7 — так же, как на 3.13.
3.14Появились t-строки: t"…" даёт объект Template, а не строку. На 3.13 и раньше это SyntaxError.
3.15Вышел 9 октября 2026; урок на нём не запускался. По release notes, Python now uses UTF-8 as the default encoding, independent of the system's environment (Python теперь по умолчанию использует UTF-8 независимо от окружения системы); прежнее поведение — PYTHONUTF8=0 или -X utf8=0. Явное encoding="utf-8" по-прежнему нужно коду, который работает и на версиях до 3.15.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Строку скопировали в другое имя, потом дописали к исходной. Что напечатает этот код?
s = "ab"
t = s
s += "c"
print(s, t, s is t)

Практика · оцените

Сколько байт занимает строка 'привет, мир' в кодировке UTF-8?
байт

Проверьте себя

Вопрос 1 из 4

Что даст 'Python'[10:20]?

Чем измерено

Числа и наблюдения этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.

Прогоны: CPython 3.11.15, 3.12.3, 3.13.7 и 3.14.7, Linux x86_64, одна машина (2 ядра); цитаты документации и исходников сверены 14.10.2026. Python 3.15 к этой дате вышел, но в среде замеров его нет.

Источники и что читать дальше

6 ИСТОЧНИКОВ

  1. Модель данных — неизменяемые последовательностиОфициальная документация. Что значит неизменяемость: «An object of an immutable sequence type cannot change once it is created» (Объект неизменяемой последовательности не может измениться после создания). Строки названы в списке таких типов первыми. Сверено по встроенной справке pydoc (модуль pydoc_data.topics) CPython 3.13.7 и 3.14.7.https://docs.python.org/3.13/reference/datamodel.html#immutable-sequences
  2. Встроенные типы — операции над последовательностями, примечание 6Официальная документация. Почему склейка в цикле дорогая: «Concatenating immutable sequences always results in a new object. This means that building up a sequence by repeated concatenation will have a quadratic runtime cost in the total sequence length» (Склейка неизменяемых последовательностей всегда даёт новый объект. Поэтому построение последовательности повторной склейкой обходится во время, квадратичное от её итоговой длины). И совет: «if concatenating str objects, you can build a list and use str.join() at the end or else write to an io.StringIO instance and retrieve its value when complete» (при склейке строк можно собрать список и в конце вызвать str.join() или писать в io.StringIO и забрать значение, когда всё готово). Сверено по pydoc_data.topics 3.13.7 и 3.14.7.https://docs.python.org/3.13/library/stdtypes.html#common-sequence-operations
  3. Справочник языка — f-строки и t-строкиОфициальная документация. История f-строк: «Changed in version 3.8: Added the debug specifier ("=") Changed in version 3.12: Many restrictions on expressions within f-strings have been removed. Notably, nested strings, comments, and backslashes are now permitted» (Изменено в 3.8: добавлен отладочный спецификатор „=“. Изменено в 3.12: снято многое из ограничений на выражения внутри f-строк — в частности, теперь разрешены вложенные строки, комментарии и обратная косая черта). О t-строках: «Added in version 3.14. A template string literal or t-string is a string literal that is prefixed with ‘t’ or ‘T’» (Добавлено в 3.14. Шаблонный строковый литерал, или t-строка, — литерал с префиксом t или T). Сверено по pydoc_data.topics 3.14.7; во встроенной справке 3.13.7 раздела об f-строках нет.https://docs.python.org/3.14/reference/lexical_analysis.html#f-strings
  4. Мини-язык спецификации форматаОфициальная документация. Как форматирование числа округляет: «The rounding mode for float matches that of the round() builtin» (Режим округления для float тот же, что у встроенной round()). Сверено по pydoc_data.topics 3.13.7 и 3.14.7.https://docs.python.org/3.13/library/string.html#format-specification-mini-language
  5. Python/bytecodes.c — быстрая склейка BINARY_OP_INPLACE_ADD_UNICODEИсходный код CPython. Строки 544–553 на теге v3.13.7 (на v3.14.0 — тот же комментарий в строках 771–780). Зачем быстрый путь: «When possible, extend left in place rather than allocating a new PyUnicodeObject. This attempts to avoid quadratic behavior when one neglects to use str.join()» (Когда можно, дописать left на месте, а не создавать новый объект строки. Это попытка избежать квадратичного поведения, когда забыли применить str.join()). И условие: «If left has only two references remaining (one from the stack, one in the locals), DECREFing left leaves only the locals reference, so PyUnicode_Append knows that the string is safe to mutate» (Если у left осталось ровно две ссылки — со стека и из локальной переменной, — то после уменьшения счётчика остаётся только локальная, и PyUnicode_Append знает, что строку можно менять на месте).https://github.com/python/cpython/blob/v3.13.7/Python/bytecodes.c
  6. Что нового в Python 3.15 — UTF-8 по умолчаниюОфициальная документация. Python 3.15 вышел 9 октября 2026. Release notes: «Python now uses UTF-8 as the default encoding, independent of the system's environment» (Теперь Python по умолчанию использует кодировку UTF-8 независимо от окружения системы); прежнее поведение возвращают PYTHONUTF8=0 или -X utf8=0. Цитата сверена 14.10.2026. Урок на 3.15 не запускался: в среде замеров этой версии нет.https://docs.python.org/3.15/whatsnew/3.15.html