Строки и форматирование: неизменяемость, срезы, 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:
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: неизменяемость и +=
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: срезы и индексы
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: методы, о которых спрашивают
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: форматирование
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
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
Документация предупреждает прямо:
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.
Склейка неизменяемых последовательностей всегда даёт новый объект. Поэтому построение последовательности повторной склейкой обходится во время, квадратичное от её итоговой длины.
Замер: 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)
Практика · оцените
Проверьте себя
Что даст 'Python'[10:20]?
Чем измерено
Числа и наблюдения этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
bench/strfmt/example.pybench/strfmt/basics.pybench/strfmt/cost.pybench/strfmt/fastpath.pybench/strfmt/practice.py
Прогоны: CPython 3.11.15, 3.12.3, 3.13.7 и 3.14.7, Linux x86_64, одна машина (2 ядра); цитаты документации и исходников сверены 14.10.2026. Python 3.15 к этой дате вышел, но в среде замеров его нет.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Строка в 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 урок не запускал.
На самом деле
- Связывает имя с новой строкой. После
t = s; s += '!'имяtвидит старое значение,s is t—False. - Границы среза урезаются до длины строки:
p[10:20]даёт пустую строку,p[:100]— всю.IndexErrorбросает только индекс:p[10]. - Печатает
2: значение ровно посередине округляется к чётному.round(2.5)— тоже2, аround(3.5)—4. - Документация называет её квадратичной, но CPython при ряде условий дописывает строку на месте — тогда на размерах замера время на кусок не росло. Условия — деталь версии: лишняя ссылка дала ×13,6 на кусок при ×10 кусков, а на 3.13 и 3.14 квадрат возвращала даже запись
s += x; k += 1в одну строку. Надёжный путь —join. - Длина в кодовых точках.
'привет'— 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; прежнее поведение —
PYTHONUTF8=0или-X utf8=0. Явноеencoding="utf-8"по-прежнему нужно коду, который работает и на версиях до 3.15.<
Что разобрано
- База: строка — неизменяемая последовательность
- Механизм 1: неизменяемость и +=
- Механизм 2: срезы и индексы
- Механизм 3: методы, о которых спрашивают
- Механизм 4: форматирование
- Механизм 5: str и bytes
- Глубже: цена склейки и быстрый путь CPython
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверьте себя
- Чем измерено
Источники и что читать дальше
6 ИСТОЧНИКОВ
- Модель данных — неизменяемые последовательностиОфициальная документация. Что значит неизменяемость: «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
- Встроенные типы — операции над последовательностями, примечание 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
- Справочник языка — 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
- Мини-язык спецификации форматаОфициальная документация. Как форматирование числа округляет: «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
- Python/bytecodes.c — быстрая склейка BINARY_OP_INPLACE_ADD_UNICODEИсходный код CPython. Строки 544–553 на теге v3.13.7 (на v3.14.0 — тот же комментарий в строках 771–780). Зачем быстрый путь: «When possible, extend
leftin place rather than allocating a new PyUnicodeObject. This attempts to avoid quadratic behavior when one neglects to use str.join()» (Когда можно, дописать left на месте, а не создавать новый объект строки. Это попытка избежать квадратичного поведения, когда забыли применить str.join()). И условие: «Iflefthas only two references remaining (one from the stack, one in the locals), DECREFingleftleaves 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 - Что нового в 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