Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Строки и форматирование
strings

Строки и форматирование

Строки, f-строки, методы строк, работа с текстом

Открыть лабораториюv0.6.2Запускается локально из публичного репозитория

Строки и форматирование в Python

  • Маршрут: Основы · тема 5 из 7
  • Навыки: J2, J5, M7
  • До урока: basic_types, control_flow, functions.
  • Результат: корректная обработка текста на границах Unicode, байтов и внешних форматов.
  • Основа: операции со строками и форматирование. Работа в эксплуатации: кодировки, нормализация, регулярные выражения и безопасный разбор. Углубление: правила Unicode, стоимость преобразований и границы версий.
  • Подтверждение: нормализатор текстовой границы и набор Unicode-тестов.

Строка в Python — неизменяемая последовательность Unicode-кодовых точек. Методы не меняют исходную строку, а возвращают новое значение. На границе с файлом или сетью текст нужно явно кодировать в байты и декодировать обратно.

#1. Базовые операции

s = "hello" s + " world" # "hello world" — новая строка s * 3 # "hellohellohello" len(s) # 5 s[0] # 'h' — доступ по индексу (но не изменение!)

Изменить символ по индексу нельзя:

s[0] = 'H' # TypeError: 'str' object does not support item assignment

Соберите новую строку:

s = "H" + s[1:] # "Hello"

#2. Методы строк

МетодПримерРезультат
.upper()"abc".upper()"ABC"
.lower()"ABC".lower()"abc"
.casefold()"Straße".casefold()"strasse"
.strip()" x ".strip()"x"
.split()"a,b,c".split(",")["a", "b", "c"]
.join()",".join(["a","b"])"a,b"
.replace()"hello".replace("l", "L")"heLLo"
.startswith() / .endswith()"file.txt".endswith(".txt")True

Когда частей много, обычно проще и предсказуемее собрать их через .join():

# Повторное создание промежуточных строк result = "" for word in words: result += word + " " # Один вызов для заранее собранных частей result = " ".join(words)

#casefold() vs lower() — сравнение без регистра для Unicode

lower() — простое преобразование к нижнему регистру:

"HELLO".lower() # "hello" "Straße".lower() # "straße" (немецкая эсцет) "ΑΛΕΞΑΝΔΡΟΣ".lower() # "αλεξανδρος" (греческий)

casefold() — агрессивное сравнение без регистра для Unicode (Python 3.0+):

"HELLO".casefold() # "hello" "Straße".casefold() # "strasse" (эсцет → "ss") "ΑΛΕΞΑΝΔΡΟΣ".casefold() # "αλεξανδρος"

#Когда использовать casefold()

Используйте casefold() для сравнения строк без учёта регистра:

# Проблема с lower() "straße".lower() == "Strasse".lower() # False # Решение с casefold() "straße".casefold() == "Strasse".casefold() # True # Сравнение имён пользователей, email и т.д. def is_same_user(input1: str, input2: str) -> bool: return input1.casefold() == input2.casefold()

Для ASCII-строк разницы нет:

"Hello".lower() == "Hello".casefold() # True

Не используйте casefold(), если:

  • Нужно сохранить оригинальные символы (например, для отображения пользователю)
  • Работаете только с ASCII и важна производительность (разница минимальна)

#Практический пример

# Поиск по базе пользователей без учёта регистра users = ["Alice", "БОРИС", "straße", "Strasse"] search = "STRASSE" matches = [u for u in users if u.casefold() == search.casefold()] # matches = ["straße", "Strasse"] # С lower() не сработало бы для немецкого matches_lower = [u for u in users if u.lower() == search.lower()] # matches_lower = []

Для Unicode-сравнения без учёта регистра casefold() обычно подходит лучше lower(). Но он не заменяет нормализацию Unicode и правила конкретного домена: например, идентификаторы пользователей и адреса электронной почты могут иметь собственный контракт сравнения.

#Лексикографический порядок

Операторы <, <=, > и >= сравнивают строки лексикографически по Unicode- кодовым точкам. Это стабильное техническое сравнение, но не алфавитная сортировка для языка пользователя.

assert "Z" < "a" assert "apple" < "banana" assert "abc" < "abcd"

Для человекочитаемой сортировки нужен отдельный collation-механизм. Модуль locale предоставляет locale.strxfrm(), но опирается на глобальную настройку процесса. В серверном приложении часто выбирают библиотеку с Unicode Collation Algorithm, например PyICU, и явно фиксируют локаль.

#3. Форматирование строк

#a) %-форматирование (устаревшее)

"name: %s, age: %d" % ("Alice", 30)

#b) .format() (Python 2.6+)

"name: {}, age: {}".format("Alice", 30) "name: {name}, age: {age}".format(name="Alice", age=30)

#c) f-строки (Python 3.6+, рекомендуется)

name = "Alice" age = 30 f"name: {name}, age: {age}" # "name: Alice, age: 30" f"double age: {age * 2}" # "double age: 60" f"{name.upper()}" # "ALICE" f"{value:.2f}" # форматирование чисел

f-строки быстрее, читабельнее и поддерживают выражения внутри {}.

#d) string.Template — простой синтаксис шаблонов

string.Template подставляет только имена вида $name и не вычисляет Python-выражения из шаблона. Это удобно, когда текст редактирует не разработчик. Безопасность всей операции всё равно зависит от того, куда попадёт результат: для HTML, SQL и shell нужны соответствующие экранирование и параметризация.

#Базовое использование

from string import Template # Создание шаблона t = Template("Привет, $name! Ваш баланс: $balance руб.") # Подстановка значений result = t.substitute(name="Alice", balance=1000) # "Привет, Alice! Ваш баланс: 1000 руб." # Или через словарь data = {"name": "Bob", "balance": 500} result = t.substitute(data)

#$variable vs ${variable}

# $name — простая подстановка t = Template("Привет, $name!") # ${variable} — когда имя переменной граничит с другими символами t = Template("Цена: ${price}₽ (не $price₽)") t.substitute(price=100) # "Цена: 100₽ (не $price₽)"

#Строгая и мягкая подстановка

.substitute(**kwargs) выбрасывает KeyError, если переменная не найдена, а .safe_substitute(**kwargs) оставляет $var в тексте как есть.

t = Template("Привет, $name!") t.substitute(name="Alice") # "Привет, Alice!" t.substitute() # KeyError: 'name' t.safe_substitute() # "Привет, $name!" (без ошибки)

#Когда использовать string.Template

Подходящие случаи:

  • Редактируемые шаблоны — автору доступны только простые $name-поля
  • Конфигурационные файлы — простые шаблоны без сложной логики
  • Ограниченный синтаксис — шаблон не вычисляет произвольные Python-выражения
  • Интернационализация (i18n) — переводчики могут безопасно редактировать шаблоны

Не используйте Template для:

  • Сложной логики форматирования (выберите .format() или f-строки)
  • Форматирования чисел, дат (нет встроенной поддержки)
  • Когда нужна производительность (медленнее f-строк)

#Пример: редактируемый шаблон письма

from string import Template # Пользовательский шаблон (может прийти из БД или конфига) user_template = "Уважаемый $customer, ваш заказ $order_id готов!" # Шаблон поддерживает только объявленные имена t = Template(user_template) email = t.safe_substitute(customer="Иван", order_id=12345) # "Уважаемый Иван, ваш заказ 12345 готов!" # `.format()` не исполняет произвольный Python-код, но разрешает более сложный # доступ к атрибутам и элементам. Для внешнего шаблона лучше узкий синтаксис.

#Сравнение подходов

name = "Alice" # f-строки — когда выражение пишет разработчик f"Hello, {name}!" # .format() — когда нужен форматный мини-язык "Hello, {}!".format(name) # Template — когда нужен ограниченный набор именованных полей from string import Template Template("Hello, $name!").substitute(name=name)

По умолчанию используйте f-строки в исходном коде. Для редактируемого шаблона с простыми именованными полями подходит string.Template.

#4. Модуль textwrap — форматирование текста

Модуль textwrap предназначен для форматирования и обёртывания текста (разбиение на строки определённой ширины).

#Базовое использование

import textwrap text = "Это длинный текст, который нужно разбить на несколько строк определённой ширины." # Обёртывание текста (возвращает строку) wrapped = textwrap.fill(text, width=40) print(wrapped)

Вывод:

Это длинный текст, который нужно
разбить на несколько строк
определённой ширины.

#Основные функции

ФункцияОписание
textwrap.fill(text, width)Обёртывает текст, возвращает строку
textwrap.wrap(text, width)Обёртывает текст, возвращает список строк
textwrap.dedent(text)Удаляет общий ведущий пробел (незаменимо для docstrings)
textwrap.indent(text, prefix)Добавляет префикс к каждой строке
textwrap.shorten(text, width)Сокращает текст до заданной длины

#Примеры

#wrap() — разбиение на список строк

import textwrap text = "Python — мощный язык программирования." lines = textwrap.wrap(text, width=20) print(lines) # ['Python — мощный', 'язык', 'программирования.']

#dedent() — удаление общего отступа

import textwrap # Удобно для многострочных строк в коде text = """ Первая строка. Вторая строка. Третья строка. """ clean = textwrap.dedent(text) print(clean) # Первая строка. # Вторая строка. # Третья строка.

#indent() — добавление отступа

import textwrap text = "строка 1\nстрока 2\nстрока 3" indented = textwrap.indent(text, prefix=" ") print(indented) # строка 1 # строка 2 # строка 3 # С условием (Python 3.8+) indented_if = textwrap.indent(text, prefix="> ", predicate=lambda line: True)

#shorten() — сокращение текста

import textwrap text = "Это очень длинный текст для сокращения" shortened = textwrap.shorten(text, width=20, placeholder="...") print(shortened) # "Это очень..."

#Продвинутое использование: TextWrapper

Класс TextWrapper даёт больше контроля над форматированием:

from textwrap import TextWrapper wrapper = TextWrapper( width=50, initial_indent="> ", # Отступ первой строки subsequent_indent=" ", # Отступ последующих строк break_long_words=True, # Разрывать длинные слова replace_whitespace=True, # Заменять пробелы drop_whitespace=True, # Удалять лишние пробелы max_lines=3, # Максимум строк (Python 3.4+) placeholder=" [...]" # Текст для обрезки ) text = "Это очень длинный текст, который будет отформатирован с особыми параметрами." result = wrapper.fill(text) print(result)

Вывод:

> Это очень длинный текст, который будет
  отформатирован с особыми параметрами. [...]

#Практический пример: форматирование вывода в CLI

import textwrap def print_help(): help_text = """ Это справка по использованию программы. Используйте команды: start, stop, restart. Для подробной информации добавьте флаг --verbose. """ # Удаляем отступы и форматируем clean_text = textwrap.dedent(help_text).strip() formatted = textwrap.fill(clean_text, width=60) print(formatted) print_help()

#Когда использовать textwrap

textwrap подходит для:

  • Форматирование вывода в CLI-приложениях
  • Подготовка текста для email/SMS (ограничение длины строки)
  • Обработка docstrings
  • Создание красивых отчётов

textwrap не подходит для:

  • HTML/XML (используйте специализированные библиотеки)
  • Сложной вёрстки (таблицы, колонки)

Правило: для простого форматирования текста используйте textwrap.fill(), для работы с docstrings — textwrap.dedent().

#5. RAW-строки и экранирование

  • Обычная строка: \n → перевод строки, \" → кавычка
  • RAW-строка (r"") не превращает \n, \t и похожие последовательности в управляющие символы. Кавычку всё равно нужно экранировать, а строка не может заканчиваться нечётным числом обратных слешей:
path = r"C:\Users\name\file.txt" # без экранирования обратных слешей regex = r"\d{3}-\d{2}-\d{4}" # удобно для регулярных выражений # bad = r"C:\temp\" # SyntaxError: последний \ экранирует кавычку

#6. Кодировки и байты

  • Строки str в Python 3 хранят Unicode. Кодировка появляется при преобразовании в байты или записи файла. Кодировка open() без encoding= зависит от платформы, поэтому на границе явно указывайте encoding="utf-8".
  • Для работы с файлами/сетью часто нужны байты:
text = "привет" encoded = text.encode("utf-8") # -> b'\xd0\xbf\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82' decoded = encoded.decode("utf-8") # -> "привет"

Важно: Не смешивайте str и bytes:

"hello" + b"world" # TypeError!

#7. Регулярные выражения (re)

import re pattern = r"\d{3}-\d{3}-\d{4}" match = re.search(pattern, "Call 123-456-7890") if match: print(match.group()) # "123-456-7890"

#Основные функции re

import re text = "Alice: 30, Bob: 25, Carol: 35" # re.search — первое совпадение m = re.search(r"\d+", text) m.group() # '30' m.start() # 7 m.end() # 9 # re.findall — все совпадения в виде списка ages = re.findall(r"\d+", text) # ['30', '25', '35'] # re.finditer — итератор match-объектов (ленивый) for m in re.finditer(r"(\w+): (\d+)", text): name, age = m.group(1), m.group(2) print(f"{name} -> {age}") # re.sub — замена clean = re.sub(r"\s+", " ", "too many spaces") # 'too many spaces' # re.split — разделение parts = re.split(r",\s*", "a, b, c,d") # ['a', 'b', 'c', 'd'] # re.compile — предкомпилированный паттерн (быстрее при многократном использовании) EMAIL_RE = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$') EMAIL_RE.match("alice@example.com")

#Именованные группы

LOG_RE = re.compile( r'(?P<date>\d{4}-\d{2}-\d{2}) ' r'(?P<level>INFO|WARNING|ERROR) ' r'(?P<msg>.*)' ) m = LOG_RE.match("2024-01-15 ERROR Database connection failed") if m: print(m.group('date')) # '2024-01-15' print(m.group('level')) # 'ERROR' print(m.group('msg')) # 'Database connection failed' print(m.groupdict()) # словарь всех групп

#Флаги

# re.IGNORECASE — без учёта регистра re.search(r'python', 'Python 3.12', re.I) # re.MULTILINE — ^ и $ совпадают с началом/концом каждой строки re.findall(r'^\w+', 'line1\nline2\nline3', re.M) # re.DOTALL — . совпадает с \n re.search(r'start.*end', 'start\nmiddle\nend', re.S) # re.VERBOSE — комментарии в паттерне EMAIL_VERBOSE = re.compile(r""" ^ # начало строки [a-zA-Z0-9._%+-]+ # имя пользователя @ # символ @ [a-zA-Z0-9.-]+ # домен \. # точка [a-zA-Z]{2,} # TLD $ # конец строки """, re.VERBOSE)

Это базовый обзор re. Расширенные возможности — re.sub с функцией, re.compile для переиспользования, флаги re.VERBOSE для читаемых паттернов и многое другое — разобраны в stdlib.


#8. Unicode и работа с текстом

# Python 3 строки — Unicode (str = последовательность кодовых точек) s = "Привет 🌍" len(s) # 8 символов (кодовых точек) # Информация о символе import unicodedata unicodedata.name('А') # 'CYRILLIC CAPITAL LETTER A' unicodedata.category('А') # 'Lu' (Letter, uppercase) unicodedata.normalize('NFC', 'cafe\u0301') # 'café' (NFD → NFC) # Нормализация Unicode — важно для сравнения s1 = "cafe\u0301" # e + combining accent s2 = "café" # precomposed é s1 == s2 # False: разные последовательности кодовых точек unicodedata.normalize('NFC', s1) == unicodedata.normalize('NFC', s2) # True

#Строки как байты

# Кодирование text = "Hello, мир" utf8_bytes = text.encode('utf-8') # b'Hello, \xd0\xbc\xd0\xb8\xd1\x80' utf16_bytes = text.encode('utf-16') ascii_bytes = text.encode('ascii', errors='replace') # ? вместо кириллицы # Декодирование decoded = utf8_bytes.decode('utf-8') partial = b'\xff\xfe'.decode('utf-16-le', errors='ignore') # Определение кодировки (через chardet/charset-normalizer) from charset_normalizer import from_bytes result = from_bytes(unknown_bytes).best() print(result.encoding) # 'utf-8' или другой

#9. Форматирование f-строк: продвинутые возможности

# Форматные спецификаторы pi = 3.14159265 f"{pi:.2f}" # '3.14' f"{pi:>10.3f}" # ' 3.142' (выравнивание правое, ширина 10) f"{pi:<10.3f}" # '3.142 ' (левое) f"{pi:^10.3f}" # ' 3.142 ' (по центру) f"{pi:+.2f}" # '+3.14' (всегда знак) # Числа n = 1_000_000 f"{n:,}" # '1,000,000' (разделитель тысяч) f"{n:_}" # '1_000_000' (Python 3.6+) f"{n:e}" # '1.000000e+06' f"{255:#x}" # '0xff' (hex с префиксом) f"{255:08b}" # '11111111' (бинарный, 8 символов) # Дата и время from datetime import datetime now = datetime.now() f"{now:%Y-%m-%d %H:%M:%S}" # '2024-01-15 14:30:00' f"{now:%d.%m.%Y}" # '15.01.2024' # = для отладки (Python 3.8+) x = 42 f"{x=}" # 'x=42' f"{x*2=}" # 'x*2=84' # Вложенные f-строки width = 10 f"{'text':>{width}}" # ' text'

#10. Строковые операции: редкие но полезные методы

s = "Hello, World!" # .find() и .index() — поиск подстроки s.find("World") # 7 (позиция) или -1 если не найдено s.index("World") # 7 или ValueError если не найдено s.rfind("l") # 10 (поиск справа) s.count("l") # 3 (число вхождений) # .partition() и .rpartition() — разделение на 3 части "host:port".partition(":") # ('host', ':', 'port') "a/b/c".rpartition("/") # ('a/b', '/', 'c') # .translate() — замена по таблице table = str.maketrans("aeiou", "AEIOU") # маппинг символов "hello world".translate(table) # 'hEllO wOrld' # Удаление символов через translate: remove = str.maketrans("", "", "!@#$%") "p@ss!word".translate(remove) # 'pssword' # .zfill() — заполнение нулями слева "42".zfill(5) # '00042' "42".zfill(2) # '42' (не обрезает) # .center(), .ljust(), .rjust() "text".center(10, "-") # '---text---' "text".ljust(10, ".") # 'text......' "text".rjust(10) # ' text' # .expandtabs() — замена табов пробелами "a\tb\tc".expandtabs(4) # 'a b c' # Проверки "123".isdigit() # True "abc123".isalnum() # True " ".isspace() # True "Title Case".istitle() # True "UPPER".isupper() # True "lower".islower() # True

#Практика: от пробы к рабочему решению

#Шаг 1. Соберите минимальный пример

Рассматривайте текст как границу данных: кодировка относится к байтам, а нормализация — к строкам. Результат основы — функция с явно выбранной политикой Unicode и тестами на составные символы.

#Шаг 2. Проверьте поведение

import unicodedata decomposed = "Cafe\u0301" normalized = unicodedata.normalize("NFC", decomposed) assert normalized == "Café" assert "Straße".casefold() == "strasse" wire = "данные".encode("utf-8") assert wire.decode("utf-8") == "данные"

#Шаг 3. Доведите решение до рабочего сценария

Практика с подсказками: реализуйте нормализатор пользовательских меток. До кода зафиксируйте, где применяются strip, NFC и casefold, что делать с ошибкой декодирования и какой исходный текст допустимо сохранять в журнале.

#Шаг 4. Объясните внутренний механизм

Сравните число кодовых точек и видимых символов для emoji и комбинируемых знаков. Объясните, почему len() не является универсальной мерой длины текста для интерфейса или ограничения пользовательского ввода.

#Упражнения

  1. Как преобразовать список строк в одну строку, разделяя запятыми, без лишней запятой в конце?
  2. Почему и f"{[1, 2]}", и f"{set([1, 2])}" работают, но строковое представление множества не стоит использовать как стабильный формат данных? (Подсказка: __repr__, порядок и JSON.)
  3. Напишите функцию clean_text(s: str) -> str, которая удаляет все пробелы в начале и конце, заменяет последовательности пробелов на один пробел.
  4. Как безопасно вставить пользовательский ввод в SQL-запрос? (Подсказка: не через f-строки!)
  5. Почему "straße".lower() == "Strasse".lower() возвращает False, а casefold() — True?
  6. Напишите функцию, которая принимает пользовательский шаблон письма и данные, используя string.Template для безопасной подстановки.
  7. Как отформатировать многострочный docstring с помощью textwrap.dedent() и ограничить ширину строки 80 символами?

#Самопроверка

  • Для упражнения 1 ожидается ", ".join(items) без ручного удаления последней запятой.
  • Упражнение 2 должно отделять удобное представление объекта от стабильного формата обмена: для данных используйте, например, JSON и заранее задайте порядок элементов.
  • Для clean_text проверьте пустую строку, табуляцию, перевод строки и несколько пробелов; удобный ориентир — " ".join(s.split()).
  • В SQL пользовательские значения передаются параметрами драйвера. Имя таблицы или столбца нельзя безопасно превратить в значение-параметр: выбирайте его из заранее разрешённого списка.
  • Для упражнений 5–7 добавьте минимум по одному примеру с Unicode, отсутствующим ключом шаблона и исходным многострочным отступом.

#Исполняемая лаборатория

Закрепите тему в работе «Надёжная граница Unicode». Нужно реализовать строгое декодирование, NFC-нормализацию, удаление дублей через casefold() и безопасную форму ошибок.

git clone --branch v0.6.2 --depth 1 https://gitlab.potapov.me/courses/python-labs.git cd python-labs uv sync --group test uv run --group test pytest strings/tests

Далее: Исключения