Подводные камни строк
Пять ловушек, на которых спотыкается каждый: код «не работает», а причины не видно. Разбираем их на реальных примерах — и с готовыми решениями.
Редакция Питоники
Этот урок — коллекция багов, которые встречаются у каждого. Ни одна из ловушек не бросается в глаза: код выглядит правильно, запускается без ошибок и молча делает не то. После урока вы будете узнавать их мгновенно — у себя и в чужом коде.
Ловушка 1: метод отработал, а строка не изменилась
Строковые методы возвращают НОВУЮ строку. Вызов без присваивания — выброшенный результат: программа «работает», а переменная осталась прежней.
name = ' анна '
name.strip()
name.upper()
print(name)
name = name.strip().upper()
print(name)
анна АННА
Первые два вызова создали чистые строки и выбросили их. Правильно — присвоить результат той же или новой переменной. У списков такого нет: их методы меняют на месте. Отсюда путаница при переходе между типами.
Ловушка 2: == против is
== сравнивает СОДЕРЖИМОЕ — то, что нужно для строк. is сравнивает, не один ли это объект в памяти. Две одинаковые строки — разные объекты, поэтому is может дать False для одинакового текста. Правило железное: строки сравнивают только ==.
a = 'привет'
b = ''.join(['при', 'вет'])
print(a == b)
print(a is b)
True False
Ловушка 3: регистр в сравнении
Пользователь вводит «ДА», программа ищет «да» — совпадения нет. Символы разных регистров различны, поэтому перед сравнением обе стороны приводят к нижнему регистру.
answer = 'ДА'
print(answer == 'да')
print(answer.lower() == 'да')
False True
Ловушка 4: пустая строка и if
Пустая строка считается False в логическом контексте, любая непустая — True. Удобно для «ввёл или нет», но коварно: строка из ОДНИХ ПРОБЕЛОВ непуста — она True, хотя глазу между ними нет разницы. Решение — strip перед проверкой.
empty = ''
spaces = ' '
print(bool(empty), bool(spaces))
if not spaces.strip():
print('Реально пусто (после strip)')
False True Реально пусто (после strip)
Ловушка 5: невидимые пробелы по краям
Продолжение предыдущей: данные из форм и файлов часто приезжают с хвостовыми пробелами или переносами строк. Сравнение 'анна ' == 'анна' даёт False. Лекарство уже знакомо — strip по краям перед любой обработкой и сравнением.
from_form = 'анна\n'
in_base = 'анна'
print(from_form == in_base)
print(from_form.strip() == in_base)
False True
| Ловушка | Симптом | Лечение |
|---|---|---|
| Метод без присваивания | Строка «не меняется» | s = s.strip().upper() |
| is вместо == | Одинаковый текст даёт False | Для строк только == |
| Регистр | 'ДА' != 'да' | обе стороны .lower() |
| Пробелы в if | ' ' считается непустой | if not s.strip(): |
| Хвостовые пробелы | Сравнение даёт False | strip перед сравнением |
Общий знаменатель всех пяти ловушек: строка — значение, а не контейнер. Её нужно присваивать, нормализовать (strip, lower) и сравнивать содержимым. Эти пять привычек — уже половина культуры работы с текстом.
Ловушка 6: методы списков меняют, методы строк — нет
Это ловушка, которая переносится из других языков и стоит дороже всех остальных. У списка есть append, sort, reverse — они меняют список НА МЕСТЕ и возвращают None. У строк методы возвращают НОВУЮ строку. Смешение этих двух контрактов даёт код, который выглядит правильно, а работает через раз.
parts = []
for word in ['кот', 'код', 'дом']:
parts.append(word.upper())
print(parts)
s = ''
for word in ['кот', 'код', 'дом']:
s += word.upper()
print(s)
['КОТ', 'КОД', 'ДОМ'] КОТКОДДОМ
Оба результата верны, но методы у списка и строки работают по-разному. И вот где начинается настоящая беда: parts.sort() вернёт None, и код вида result = parts.sort() присвоит в result значение None — а не отсортированный список. Для строк такой ошибки нет: s.upper() честно отдаёт новую строку. Правило: методы, меняющие список на месте, вызывают без присваивания; методы строк всегда требуют присваивания.
Ловушка 7: срез не меняет исходную строку
Срезы строк работают как копирование. s[:3] берёт первые три символа и возвращает НОВУЮ строку — исходная s не меняется. Это ловушка для тех, кто переходит из языков с изменяемыми строками: s[:3] = 'abc' не заменит первые три символа, а выбросит ошибку.
Обойти это можно только пересборкой: s = s[:3] + 'abc' + s[3:]. Выглядит громоздко, зато работает. Именно поэтому в реальном коде почти всегда используют replace или список: они и понятнее, и не требуют пересборки вручную.
Ловушка 8: конкатенация в цикле
Мы разобрали это в уроке про join, но повторю, потому что ловушка выглядит невинно. s += часть внутри цикла создаёт новую строку на каждом шаге и копирует всю накопленную. На десяти элементах это незаметно, на сотнях тысяч — это минуты вместо секунд и скачки памяти.
Признак проблемы — в профилировщике вы видите memcpy или string_concat на вершине. Лечение — join по списку или генератору. Есть и промежуточный вариант: если части приходят по одной, собирайте их в список и склеивайте в конце. Но join по генератору лучше: он вообще не держит всё в памяти сразу.
Ловушка 9: сравнение с None и с пустой строкой
Проверка if not value: ловит и None, и пустую строку, и ноль, и пустой список. Это удобно, пока вы точно знаете, что придёт. Но в коде вида if not result == expected: первое not относится к результату сравнения — и это ловушка другого рода: читать такое надо медленно.
Разложим на части: not (result == expected) означает «не равно». Но not result == expected в Python разбирается как (not result) == expected — и если result пустой или None, сравнение идёт уже с другим типом. Проверка даст False там, где вы ожидали True. Пишите скобки явно: if not (result == expected): — или, что лучше, if result != expected:.
Ловушка 10: strip не спасает от невидимых символов
Мы говорили об этом в уроке про strip, но повторю в списке ловушек, потому что именно на этом чаще всего спотыкаются: strip() срезает пробельные символы и НЕ срезает zero-width space или BOM. Символ невидим, а сравнение из-за него не проходит — и найти причину можно только через repr.
| Ловушка | Симптом | Лечение |
|---|---|---|
| Метод без присваивания | Строка «не меняется» | s = s.strip().upper() |
| is вместо == | Одинаковый текст даёт False | Для строк только == |
| Регистр | 'ДА' != 'да' | обе стороны .lower() |
| Пробелы в if | ' ' считается непустой | if not s.strip(): |
| Хвостовые пробелы | Сравнение даёт False | strip перед сравнением |
| not без скобок | Логика работает не так | not (a == b) или a != b |
| Метод списка в присваивании | Переменная стала None | вызывать без присваивания |
| Невидимые символы | Сравнение не проходит | repr() для диагностики |
Общий знаменатель всех десяти ловушек: строка — это значение, а не контейнер. Её нужно присваивать, нормализовать (strip, lower) и сравнивать содержимым. Эти привычки — уже половина культуры работы с текстом. Вторая половина — проверять предположения: repr при отладке, assert в тестах, явная кодировка при чтении файлов.
И последняя мысль, которую стоит унести из урока. Все эти ловушки объединяет одно: они не падают. Код с name.upper() без присваивания, с is вместо ==, с пробелами в if — выполняется, возвращает результат и выглядит рабочим. Поймать их можно только тестами на конкретных значениях: пустая строка, строка с пробелами, другой регистр, текст с невидимым символом. Тест на «нормальном» значении такие баги пропустит.
Как ловить такие баги: три приёма
Раз ловушки не падают, ловить их приходится осознанно. Я пользуюсь тремя приёмами, и все три работают в любом проекте.
- Тест на граничных значениях: пустая строка, строка из одного пробела, другой регистр, текст с невидимым символом. Именно на них баги и живут — тест на «нормальном» значении их пропустит.
- repr() в отладке: если значение выглядит не так, как вы ожидали, печатайте repr, а не print. Разница видна сразу.
- assert в коде:
assert 'x' in sпадает на месте с понятным сообщением, пока вы ищете причину. В продакшене assert отключают флагом -O, поэтому для проверок данных нужны настоящие if.
Про третий пункт стоит сказать подробнее, потому что это частая ошибка. assert — не проверка данных, а проверка предположений разработчика. Если пользователь пришлёт строку с невидимым символом и у вас стоит assert not s.isspace(), программа упадёт с AssertionError — а надо было показать человеку понятное сообщение. Для пользовательского ввода нужны обычные if с внятным текстом ошибки; assert — для внутренних инвариантов.
И общий совет напоследок. Все десять ловушек урока сводятся к одной привычке: относитесь к строке как к значению, а не как к контейнеру. Присваивайте результат, нормализуйте перед сравнением, проверяйте содержимое, а не идентичность объекта. Пять привычек — и девять из десяти ловушек перестают существовать.
Сначала предскажи ответ в голове — это главный навык программиста.
city = 'СОЧИ'
result = city.lower()
print(city, result)
a = ' хвост'
print(a.strip() == 'хвост', a == 'хвост')
s = 'x' + 'y'
print(s == 'xy')
print(s.isalpha())
1. Почему name.strip() «не сработал»?
2. Как сравнивать строки в Python?
3. 'ДА'.lower() == 'да' — что вернёт?
4. if s: — что произойдёт, если s = ' ' (три пробела)?
5. Данные из формы 'анна\n' не равны 'анна'. Что делать?
6. Что вернёт parts.sort() для списка parts?
7. Почему s += 'x' в цикле может стать медленным?
8. Как найти причину, по которой две строки не равны, хотя выглядят одинаково?
В переменной raw = ' ДА \n' записан ответ пользователя. Нормализуйте её (уберите края) и напечатайте: совпадает ли нормализованный ответ с 'да' без учёта регистра, и длину исходной строки.
Почему sometimes a is b работает для коротких строк?
Python кэширует короткие строки-идентификаторы: два одинаковых литерала могут оказаться одним объектом. Это внутренняя оптимизация, на которую нельзя полагаться — поведение зависит от версии и способа создания строки. Для содержимого всегда ==.
Как сравнить строки без учёта регистра и пробелов сразу?
Цепочкой нормализации: a.strip().lower() == b.strip().lower(). Для серьёзных сравнений есть более точные методы (casefold), но strip+lower покрывает 95% задач.
Как сравнить строки без учёта регистра и пробелов сразу?
Цепочкой нормализации: a.strip().lower() == b.strip().lower(). Для серьёзных сравнений есть более точные методы (casefold), но strip+lower покрывает 95% задач.
Урок 5: методы регистраЧто опаснее — is или == для строк?
Опасен is: он может дать и True, и False для одинакового текста — поведение недетерминированное. == всегда сравнивает честно. Ошибка «is вместо ==» не падает, а молча врёт.
Почему s += 'x' в цикле такой медленный?
Строки неизменяемы: каждый += создаёт новую строку и копирует всю накопленную. Итог — квадратичное поведение. На коротких циклах незаметно, на больших — минуты вместо секунд. Лечение: join по списку или генератору.
Урок 10: joinКак найти невидимый символ в строке, из-за которого не проходит сравнение?
Вызовите repr(s) — он покажет непечатаемые символы escape-последовательностями: '\u200b' или '\xa0'. Затем почистите строку явно: s.replace('\u200b', ''). Метод strip() не поможет: zero-width space не считается пробельным символом.
Понравился урок? Сошлитесь на него
«Этот урок — коллекция багов, которые встречаются у каждого.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Строки · Урок 1
Строки в Python: текст как данные
Строка — самый частый тип данных в реальных задачах: имена, города, сообщения. Знакомимся с типом str: кавычки, len, конкатенация и умножение — и узнаём, почему строку нельзя переписать на месте.
Строки · Урок 20
Проект: анализатор текстового отчёта
Финальный проект курса: настоящий текстовый отчёт магазина, полный ловушек из уроков 19 — пробелы, регистр, пустые строки. Собираем анализатор, который чистит, разбирает и выводит итоги.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Исключения · Урок 12
ValueError в Python: разбор строк в числа
int('восемнадцать') — тип подходящий, а значение не помещается. Разбираем сообщение invalid literal, границы int и float и честную проверку isdigit.
python int из строкиpython приведение типов строка число
Pandas · Урок 3
Выборка данных в Pandas: loc, iloc и фильтрация строк по условию
loc и iloc, булева маска, isin и between — достаём из таблицы заказов именно те строки и столбцы, которые нужны, и не попадаемся на SettingWithCopyWarning.
loc и iloc в pandas в чём разницакак отфильтровать строки в pandas по условию
Строки · Урок 5
Регистр: lower, upper, title
Один и тот же текст бывает написан как «Аня», «АНЯ» и «аня» — для Python это три разные строки. Знакомимся с методами регистра: lower, upper, title, capitalize и swapcase.
python строки методы регистрсравнение строк без регистра python