MEASUREMENT
bench/strfmt/basics.py
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/python/strings
- How to run it
python3.11 bench/strfmt/basics.py python3.12 bench/strfmt/basics.py python3.13 bench/strfmt/basics.py python3.14 bench/strfmt/basics.py python3.13 bench/strfmt/cost.py python3.14 bench/strfmt/cost.py python3.11 bench/strfmt/practice.py python3.12 bench/strfmt/practice.py python3.13 bench/strfmt/practice.py python3.14 bench/strfmt/practice.py python3.11 bench/strfmt/example.py # и 3.12, 3.13, 3.14 python3.11 bench/strfmt/fastpath.py # и 3.12, 3.13, 3.14
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «Строки и форматирование»
| Скрипт | Что делает |
|---|---|
basics.py |
наблюдения без времени: неизменяемость и +=, срезы, методы split/join/find/index/isdigit, спецификации формата и округление к чётному, str и bytes, что компилируется на разных версиях (кавычки внутри f-строки, t-строки) |
cost.py |
цена склейки n кусков: join, s += x, s += x при второй ссылке, io.StringIO; цена одного форматирования: f-строка, str.format, %, сложение |
practice.py |
источник ответов двух практических задач урока |
example.py |
полный короткий пример из «Базы»: копируется в пустой каталог и запускается как есть (python3 example.py) |
fastpath.py |
когда быстрый путь s += x срабатывает: три формы цикла (в том числе s += x; k += 1 в одну строку), один шаг на строке в миллион символов при второй ссылке, посчитанном хеше, sys.intern, подклассе str, символе вне ASCII; опкод += после прогрева |
python3.11 bench/strfmt/basics.py
python3.12 bench/strfmt/basics.py
python3.13 bench/strfmt/basics.py
python3.14 bench/strfmt/basics.py
python3.13 bench/strfmt/cost.py
python3.14 bench/strfmt/cost.py
python3.11 bench/strfmt/practice.py
python3.12 bench/strfmt/practice.py
python3.13 bench/strfmt/practice.py
python3.14 bench/strfmt/practice.py
python3.11 bench/strfmt/example.py # и 3.12, 3.13, 3.14
python3.11 bench/strfmt/fastpath.py # и 3.12, 3.13, 3.14
Записи в runs/: без суффикса — CPython 3.13.7, остальные — с суффиксом
версии (3.11.15, 3.12.3, 3.14.7). Все прогоны — Linux x86_64, одна машина
(2 ядра); сверено 14.10.2026. Числа cost.py и fastpath.py — наблюдения
этой конфигурации: на другой машине и версии меняются и числа, и отношения,
и, как показал fastpath.py, даже набор условий быстрого пути, — мерить у
себя. Раздел 7 basics.py и скрипты example.py, fastpath.py добавлены
по внешнему аудиту урока 14.10.2026; разделы 1–6 basics.py при
перезапуске не изменились.
Что получилось
Наблюдения (разделы 1–5 одинаковы на 3.11–3.14): s[0] = 'П' —
TypeError; после t = s; s += '!' имя t видит старую строку; p[10:20] —
'', p[10] — IndexError; '-'.join([1, 2]) — TypeError; find — −1,
index — ValueError; '-42'.isdigit() — False; f'{2.5:.0f}' — '2',
round(2.5) — 2, а Decimal с ROUND_HALF_UP — 3; f'{2.675:.2f}' и round(2.675, 2) — 2.67, потому что за литералом хранится 2.67499…; 'привет' — 6 символов
и 12 байт в UTF-8, '👍' — 1 и 4.
Раздел 7 (одинаков на 3.11–3.14): p[:100] — 'Python', p[4:100] —
'on': границы среза урезаются, результат не обязательно пуст. len считает
кодовые точки: é одной точкой — 1, буквой с комбинирующим знаком U+0301 — 2,
после NFC — снова 1; палец с оттенком кожи — 2, эмодзи «семья» — 7;
развёрнутая 'cafe\u0301' начинается с отрывшегося знака. p.strip() is p,
p.replace('z', 'y') is p, p[:100] is p — True, p.upper() is p.upper() — False: совпавший результат CPython вернул тем же объектом
(наблюдение реализации, не контракт).
Быстрый путь (fastpath.py, 3.11–3.14): шаг s += x на строке в
миллион символов — 0,1 мкс без помех и 240–690 мкс со второй ссылкой,
посчитанным hash(s), после sys.intern, у подкласса str; ASCII-строка
плюс кусок с 'é' — 101–116 мкс, строка из 'é' плюс ASCII — 0,1–0,2. Цикл
s += x; k += 1 в одну строку: ×1,0 и ×1,1 на 3.11/3.12, ×32,3 на 3.13.7 и
×35,5 на 3.14.7 при ×10 кусков — компилятор 3.13 сливает запись и чтение в
STORE_FAST_LOAD_FAST, и += специализируется в BINARY_OP_ADD_UNICODE
вместо BINARY_OP_INPLACE_ADD_UNICODE.
Версии (раздел 6): f"{d["k"]}" на 3.11 — SyntaxError: f-string: unmatched '[', с 3.12 — '1'; t"x={1 + 1}" до 3.13 включительно —
SyntaxError, на 3.14 — объект Template, выражение в котором уже вычислено (2).
Склейка (медианы, 3.13.7, нс на кусок при 2 000 / 20 000 кусков по десять
символов): join 49,8 / 35,5; s += x 82,9 / 52,5; s += x со второй
ссылкой 215,7 / 2 942,0 — рост в 13,6 раза при ×10 кусков; io.StringIO
42,0 / 49,4. На 3.14.7 — та же картина, рост 9,9.
Форматирование (медианы, 3.13.7, нс): f-строка 167,1, str.format 355,9,
% 271,5, сложение через str() 190,5. На 3.14.7 сложение (131,6) быстрее
f-строки (152,2); f-строка быстрее format и % на обеих версиях.
Script
180 lines"""Строки: неизменяемость, срезы, методы, форматирование, str и bytes.
Времени здесь нет — только наблюдения. Цена склейки и форматирования — в
cost.py.
ШЕСТЬ СЮЖЕТОВ:
1. НЕИЗМЕНЯЕМОСТЬ. Символ строки не заменить; методы возвращают новую
строку; `s += x` связывает s с новой строкой, а старая, на которую
ссылается другое имя, не меняется.
2. СРЕЗЫ. Отрицательные индексы, шаг, разворот. Срез за пределами строки
даёт пустую строку, а индекс за пределами — IndexError.
3. МЕТОДЫ, О КОТОРЫХ СПРАШИВАЮТ. split с разделителем и без, join, strip,
find против index, replace со счётчиком.
4. ФОРМАТИРОВАНИЕ. Спецификации формата в f-строке, отладочное `=`,
`!r`, str.format и %. И округление при `.0f` — к чётному.
5. STR И BYTES. Длина строки — в символах, длина байтов — в байтах.
6. ВЕРСИИ. Кавычки того же вида внутри f-строки (можно с 3.12) и t-строки
(появились в 3.14) — проверяются компиляцией, чтобы скрипт работал на
всех версиях.
Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import sys
print("PY", sys.version.split()[0])
print()
def err(fn):
try:
return repr(fn())
except Exception as e: # noqa: BLE001 — нам нужен текст
return f"{type(e).__name__}: {e}"
# ── 1 ─────────────────────────────────────────────────────────────────────
print("1) неизменяемость")
s = "привет"
def assign_char():
s[0] = "П"
print(" s[0] = 'П' ->", err(assign_char))
upper = s.upper()
print(" s.upper() ->", repr(upper), "| s ->", repr(s))
t = s
s += "!"
print(" t = s; s += '!' -> s =", repr(s), "| t =", repr(t), "| s is t:", s is t)
print()
# ── 2 ─────────────────────────────────────────────────────────────────────
print("2) срезы")
p = "Python"
for expr in ["p[0]", "p[-1]", "p[1:4]", "p[:2]", "p[-3:]", "p[::2]", "p[::-1]", "p[10:20]", "p[10]"]:
print(f" {expr:<10}->", err(lambda expr=expr: eval(expr)))
print()
# ── 3 ─────────────────────────────────────────────────────────────────────
print("3) методы")
cases = [
("'a,b,,c'.split(',')", lambda: "a,b,,c".split(",")),
("' a b '.split()", lambda: " a b ".split()),
("'-'.join(['a', 'b', 'c'])", lambda: "-".join(["a", "b", "c"])),
("'-'.join([1, 2])", lambda: "-".join([1, 2])),
("' hi '.strip()", lambda: " hi ".strip()),
("'abc'.find('z')", lambda: "abc".find("z")),
("'abc'.index('z')", lambda: "abc".index("z")),
("'hello'.replace('l', 'L', 1)", lambda: "hello".replace("l", "L", 1)),
("'hello world'.title()", lambda: "hello world".title()),
("'42'.isdigit(), '-42'.isdigit()", lambda: ("42".isdigit(), "-42".isdigit())),
]
for label, fn in cases:
print(f" {label:<34}->", err(fn))
print()
# ── 4 ─────────────────────────────────────────────────────────────────────
print("4) форматирование")
x = 3.14159
n = 1234567
name = "Аня"
print(" f'{x:.2f}' ->", repr(f"{x:.2f}"))
print(" f'{n:,}' ->", repr(f"{n:,}"))
print(" f'{n:_}' ->", repr(f"{n:_}"))
print(" f'{name:>6}|' ->", repr(f"{name:>6}|"))
print(" f'{name:*^9}' ->", repr(f"{name:*^9}"))
print(" f'{name!r}' ->", repr(f"{name!r}"))
print(" f'{x=}' ->", repr(f"{x=}"))
print(" f'{x=:.2f}' ->", repr(f"{x=:.2f}"))
print(" f'{0.25:.0%}' ->", repr(f"{0.25:.0%}"))
print(" '{} и {}'.format(1, 2) ->", repr("{} и {}".format(1, 2)))
print(" '%s: %d' % ('n', 5) ->", repr("%s: %d" % ("n", 5)))
print(" f'{0.5:.0f} {1.5:.0f} {2.5:.0f}' ->", repr(f"{0.5:.0f} {1.5:.0f} {2.5:.0f}"))
print(" round(2.5), round(3.5) ->", round(2.5), round(3.5))
from decimal import ROUND_HALF_UP, Decimal # noqa: E402
print(" Decimal('2.5').quantize(Decimal('1'), rounding=ROUND_HALF_UP) ->",
Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP))
print(" f'{2.675:.2f}', round(2.675, 2) ->", repr(f"{2.675:.2f}"), round(2.675, 2))
print(" Decimal(2.675) ->", Decimal(2.675))
print()
# ── 5 ─────────────────────────────────────────────────────────────────────
print("5) str и bytes")
for word in ["hello", "привет", "👍"]:
b = word.encode("utf-8")
print(f" {word!r:<10} len = {len(word)}, в UTF-8 байт = {len(b)}")
b = "привет".encode()
print(" 'привет'.encode()[0] ->", b[0], "— байт, это int, а не символ")
print(" b.decode() ->", repr(b.decode()))
print(" 'привет'.encode('ascii') ->", err(lambda: "привет".encode("ascii")))
print(" 'a' + b'b' ->", err(lambda: "a" + b"b"))
print()
# ── 6 ─────────────────────────────────────────────────────────────────────
print("6) версии: что компилируется")
d = {"k": 1}
def compiles(src):
try:
code = compile(src, "<src>", "eval")
except SyntaxError as e:
return f"SyntaxError: {e.msg}"
value = eval(code)
if type(value).__name__ == "Template":
return f"Template, значение выражения уже вычислено: {value.interpolations[0].value}"
return repr(value)
print(" f\"{d[\"k\"]}\" ->", compiles('f"{d["k"]}"'))
print(" f\"{d['k']}\" ->", compiles("f\"{d['k']}\""))
print(" t\"x={1 + 1}\" ->", compiles('t"x={1 + 1}"'))
print()
# ── 7 ─────────────────────────────────────────────────────────────────────
# Добавлен по внешнему аудиту урока (14.10.2026). Разделы 1–6 выше не менялись.
#
# ТРИ УТОЧНЕНИЯ. Срез за границей не обязательно пуст: границы урезаются до
# длины строки. len считает кодовые точки Unicode, а не видимые символы:
# 'é', записанная буквой e и комбинирующим знаком U+0301, — две точки; и
# разворот [::-1] переставляет точки, отрывая знак от буквы. А «метод
# возвращает новую строку» — неточно: неизменяемость обещает только, что
# исходная не изменится; CPython может вернуть тот же объект, если результат
# с ним совпал. Входы с не-ASCII символами записаны escape-последовательностями
# и печатаются через ascii(), чтобы строка вывода читалась одинаково везде.
print("7) границы среза, кодовые точки, тот же объект")
import unicodedata # noqa: E402
p = "Python"
for expr in ["p[:100]", "p[4:100]", "p[-100:2]"]:
print(f" {expr:<10}->", err(lambda expr=expr: eval(expr)))
FAMILY = "\U0001F468\u200d\U0001F469\u200d\U0001F467\u200d\U0001F466"
print(" FAMILY =", ascii(FAMILY))
uni = [
("len('\\u00e9')", lambda: len("\u00e9")),
("len('e\\u0301')", lambda: len("e\u0301")),
("len(unicodedata.normalize('NFC', 'e\\u0301'))", lambda: len(unicodedata.normalize("NFC", "e\u0301"))),
("len('\\U0001F44D\\U0001F3FD')", lambda: len("\U0001F44D\U0001F3FD")),
("len(FAMILY)", lambda: len(FAMILY)),
("ascii('cafe\\u0301'[::-1])", lambda: ascii("cafe\u0301"[::-1])),
]
for label, fn in uni:
print(f" {label:<46}->", fn())
same = [
("p.strip() is p", lambda: p.strip() is p),
("p.replace('z', 'y') is p", lambda: p.replace("z", "y") is p),
("p[:100] is p", lambda: p[:100] is p),
("p.upper() is p.upper()", lambda: p.upper() is p.upper()),
]
for label, fn in same:
print(f" {label:<46}->", fn())