Проект на функциях Python: библиотека утилит для текста
Итоговый проект раздела: своя мини-библиотека утилит для текста — normalize, word_count, top_words. Три куска, каждый запускается и проверяется отдельно.
Редакция Питоники
Финальный проект собирает раздел в одну программу: библиотека утилит для текста. Библиотека утилит начинается с маленьких функций: каждая решает одну задачу, возвращает результат и несёт свою документацию. Три утилиты — normalize приводит текст в порядок, word_count считает слова, top_words находит самые частые. У каждой — docstring из урока про документацию, умолчания из урока про умолчания и честный return из урока про return. Проект строится тремя самостоятельными кусками: каждый кусок объявляет свои функции, печатает свою демонстрацию и заканчивается мини-проверками — его можно запустить отдельно.
| Утилита | Что делает | Приёмы внутри |
|---|---|---|
| normalize | Схлопывает пробелы, приводит регистр | split, join, аргумент-флаг с умолчанием |
| word_count | Считает слова в строке | split, len |
| top_words | Возвращает самые частые слова | Счётчик-словарь, sorted с key |
| truncate | Обрезает длинный текст с хвостом | Срез, умолчания |
Данные глазами
Любой проект начинается с взгляда на данные. Наш сырьё — грязный текст: двойные пробелы, пробелы по краям, разнобой регистра. Печать через repr покажет строку с точностью до символа — невидимые пробелы станут видимыми:
raw = ' Кот пришёл кот ушёл кот вернулся '
print(repr(raw))
print(raw.split())
' Кот пришёл кот ушёл кот вернулся ' ['Кот', 'пришёл', 'кот', 'ушёл', 'кот', 'вернулся']
repr показал пробелы по краям и двойные внутри, а split уже разобрал текст на слова: он режет по любым пробельным символам и игнорирует края. На этом и построятся первые две утилиты.
Кусок 1: normalize и word_count
Первая утилита чистит текст: схлопывает повторы пробелов в один и приводит к нижнему регистру. Регистр — по желанию: за это отвечает аргумент-флаг lower с умолчанием. Вторая утилита считает слова. Обе короткие, обе с docstring, а после демонстрации — мини-проверки:
def normalize(text, lower=True):
"""Чистит текст: схлопывает пробелы и приводит регистр.
Аргументы:
text: исходная строка.
lower: приводить ли к нижнему регистру.
Возвращает:
очищенную строку.
"""
cleaned = ' '.join(text.split())
if lower:
cleaned = cleaned.lower()
return cleaned
def word_count(text):
"""Считает слова в строке."""
return len(text.split())
dirty = ' Привет мир и ВСЕ '
print(normalize(dirty))
print(normalize(dirty, lower=False))
print(word_count(dirty))
print(word_count(''))
assert normalize(' A B ') == 'a b'
assert normalize(' A B ', lower=False) == 'A B'
assert word_count('один два три') == 3
assert word_count('') == 0
print('Проверки пройдены')
привет мир и все Привет мир и ВСЕ 4 0 Проверки пройдены
Функция normalize собрана из одного приёма: split разорвал строку по пробельным, join склеил слова обратно одинарными пробелами. Флаг lower — умолчание из урока про умолчания: не передали — приводим регистр, передали False — оставляем как есть. Проверки на assert — строки-приговоры: условие верно — программа молчит и идёт дальше, условие ложно — падение с AssertionError. Четыре проверки молча прошли, и напечатался вердикт.
def normalize(text, lower=True):
...
def normalize(text, lower=True):
"""Чистит текст: схлопывает пробелы и приводит регистр."""
cleaned = ' '.join(text.split())
if lower:
cleaned = cleaned.lower()
return cleaned
print(normalize('Раз ДВА'))
print(normalize('Раз ДВА', lower=True))
print(normalize('Раз ДВА', lower=False))
раз два раз два Раз ДВА
Первые две строки вывода совпали: умолчание — это значение, которое Python подставляет за вас. Третья строка — тот же вызов с явным False: регистр сохранён. Проектируя утилиту, думайте о самом частом вызове — его сценарий и становится умолчанием.
Кусок 2: top_words — частотный топ
Третья утилита сложнее: она считает, сколько раз встретилось каждое слово, и возвращает самые частые. Внутри — знакомые детали: счётчик-словарь с get из курса про словари, сортировка по своему правилу из урока про sorted с key и срез:
def top_words(text, n=3):
"""Возвращает n самых частых слов текста.
Аргументы:
text: исходная строка.
n: сколько слов вернуть.
Возвращает:
список пар «слово — счёт» от частых к редким.
"""
counts = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
pairs = sorted(counts.items(), key=lambda pair: pair[1], reverse=True)
return pairs[:n]
text = 'кот пёс кот кот пёс рыба'
print(top_words(text))
print(top_words(text, n=2))
print(top_words(text, n=1))
assert top_words(text, n=1) == [('кот', 3)]
assert top_words(text, n=2) == [('кот', 3), ('пёс', 2)]
print('Проверки пройдены')
[('кот', 3), ('пёс', 2), ('рыба', 1)]
[('кот', 3), ('пёс', 2)]
[('кот', 3)]
Проверки пройденыpairs = sorted(counts.items(), key=lambda pair: pair[1], reverse=True)
Утилита гибкая: один и тот же вызов с разными n отвечает на разные вопросы — полный расклад, пара лидеров, единственный чемпион. Умолчание n=3 подобрано под самый ходовой случай — топ-3.
Кусок 3: библиотека целиком
Куски 1 и 2 можно просто сложить в один файл — и получится библиотека. Дополнительная функция text_report не считает сама ни одного слова: она вызывает соседей. Так работает библиотека: функции строятся друг на друге, вместо повторения кода:
def normalize(text, lower=True):
"""Чистит текст: схлопывает пробелы и приводит регистр."""
cleaned = ' '.join(text.split())
if lower:
cleaned = cleaned.lower()
return cleaned
def word_count(text):
"""Считает слова в строке."""
return len(text.split())
def top_words(text, n=3):
"""Возвращает n самых частых слов текста."""
counts = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
pairs = sorted(counts.items(), key=lambda pair: pair[1], reverse=True)
return pairs[:n]
def text_report(raw):
"""Печатает мини-отчёт по сырому тексту."""
clean = normalize(raw)
print('Очищенный текст:', clean)
print('Слов:', word_count(clean))
print('Топ-1:', top_words(clean, n=1))
text_report(' Три кота пили молоко Три кота ')
assert normalize(' X Y ') == 'x y'
assert word_count('a b c') == 3
assert top_words('день ночь день', n=1) == [('день', 2)]
print('Проверки пройдены')
Очищенный текст: три кота пили молоко три кота
Слов: 6
Топ-1: [('три', 2)]
Проверки пройденыОтчёт собрался тремя вызовами: normalize почистил сырьё, word_count посчитал шесть слов, top_words нашёл лидера. Обратите внимание на Топ-1: слова три и кота встретились поровну, дважды каждый. Победила три — при равных счётах sorted оставляет порядок первого появления: слово три встретилось в тексте раньше. Это свойство сортировки называется стабильностью, и проект на него аккуратно опирается.
Бонус: библиотека растёт
Библиотека добавляется новыми утилитами по одному шаблону: docstring, при необходимости умолчания, одна задача, return. Две новые функции — обрезка длинных заголовков и капитализация слов:
def truncate(text, limit=12, tail='...'):
"""Обрезает длинный текст и добавляет хвост."""
if len(text) <= limit:
return text
return text[:limit] + tail
def title_case(text):
"""Каждое слово с заглавной буквы."""
return ' '.join(word.capitalize() for word in text.split())
print(truncate('Короткий'))
print(truncate('Длинный заголовок новости'))
print(truncate('Длинный заголовок новости', limit=7, tail='!'))
print(title_case('привет мир из python'))
assert truncate('Короткий') == 'Короткий'
assert truncate('Длинный заголовок новости') == 'Длинный заго...'
assert title_case('') == ''
print('Проверки пройдены')
Короткий Длинный заго... Длинный! Привет Мир Из Python Проверки пройдены
Обрезка работает в два ответвления: короткая строка возвращается нетронутой, длинная — режется срезом до лимита и получает хвост. У truncate два умолчания: лимит двенадцать и хвост из трёх точек, а явные аргументы перекрывают их — вызов с limit=7 и tail='!' обрезал заголовок до одного слова. Капитализация собрана генератором: каждое слово метод capitalize ставит с заглавной, а остальной регистр внутри слова сбрасывает. Четыре печати отработали, четыре проверки промолчали — и вердикт закрыл кусок.
- def и тело с отступом — каркас каждой функции из первого урока;
- аргументы и умолчания — нижний регистр по желанию, хвост у обрезки, размер топа;
- docstring — визитка каждой утилиты;
- return — утилиты возвращают результат, печатает отчёт; печать из функций-утилит не нужна;
- циклы и счётчики — top_words считает частоты, как в уроке про счётчики;
- sorted с key и срез — отбор лидеров; Counter из урока про Counter сделает тот же счёт одной строкой.
Проект окончен — и с ним весь раздел: от первой def до библиотеки, где функции имеют документацию, умолчания, проверки и друг друга. Дальше — курс ООП: классы упакуют такие библиотеки ещё плотнее, а функции станут методами. Все двадцать уроков остаются под рукой — возвращайтесь к ним как к справочнику.
Демонстрация отдельно от библиотеки
Весь проект поместился в один файл — и это правильно на старте. Импорты, порядок объявлений, пакеты — всё это лишняя сложность, пока в библиотеке пять функций. Мы держим правило: пока код помещается на один экран, он живёт в одном файле; когда перестаёт — делим по темам.
Но демонстрацию стоит отделить от самой библиотеки. Для этого в Python есть особая строка: if __name__ == '__main__':. Когда файл запускают как программу, блок под ней выполняется; когда его импортируют — нет. Так библиотеку можно положить в проект и не получить при импорте лишнего вывода на экране.
def word_count(text):
"""Считает слова в строке."""
return len(text.split())
if __name__ == '__main__':
print(word_count('раз два три'))
print(word_count(' четыре пять '))
3 2
При запуске файла мы увидели оба ответа. При импорте from text_utils import word_count выполнится только определение функции, а печать под главным блоком — нет. В нашем проекте демонстрации стояли прямо после функций, и это работало — но в файле, который будут импортировать, главный блок обязателен.
Бонус: slugify и strip_punctuation
Две утилиты, о которых мы говорили в самом начале, пишутся по тому же шаблону. Первая превращает заголовок в адрес страницы — это то, что делает любой блоговый движок.
def slugify(title, sep='-'):
"""Превращает заголовок в адрес страницы."""
words = title.lower().split()
return sep.join(words)
def strip_punctuation(text):
"""Убирает знаки препинания из текста."""
result = ''
for char in text:
if char.isalnum() or char.isspace():
result += char
return result
print(slugify('Мой Первый Пост'))
print(slugify('Отчёт за Декабрь'))
print(strip_punctuation('Привет, мир! Как дела?'))
assert slugify('Мой Первый Пост') == 'мой-первый-пост'
assert strip_punctuation('а, б!') == 'а б'
print('Проверки пройдены')
мой-первый-пост отчёт-за-декабрь Привет мир Как дела Проверки пройдены
Slugify устроена из трёх приёмов, которые мы уже знаем: lower приводит регистр, split режет по пробельным, join склеивает разделителем. Разделитель — умолчание, поэтому простые адреса получаются одной строкой. Strip_punctuation идёт по символам и оставляет только буквы, цифры и пробелы: методы isalnum и isspace отвечают на вопрос «этот символ можно оставить». Обе утилиты проверяются assert-ом — так же, как остальные.
| Правило нашей библиотеки | Почему именно так |
|---|---|
| Одна функция — одна задача | Иначе её невозможно переиспользовать и трудно проверить |
| Функция возвращает, а не печатает | Результат можно передать дальше, печать — дело вызывающего |
| У каждой функции есть docstring | Визитка дешевле вопросника через полгода |
| Значение по умолчанию для частого случая | Вызов остаётся коротким и читается как инструкция |
| Пара assert-проверок после функции | Ломается громко и сразу, а не через три экрана |
| Имя читается как команда | normalize, word_count, top_words — видно, что делает, без чтения тела |
Шесть правил — и библиотека собрана. Ни одно из них не про синтаксис: они про то, чтобы через полгода вы открыли файл и за десять секунд поняли, где что чинится. Код, который написан по этим правилам, читается без документации.
Сначала предскажи ответ в голове — это главный навык программиста.
def word_count(text):
"""Считает слова в строке."""
return len(text.split())
print(word_count(' раз два три '))
def top_words(text, n=1):
counts = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
pairs = sorted(counts.items(), key=lambda pair: pair[1], reverse=True)
return pairs[:n]
print(top_words('ночь день ночь'))
def truncate(text, limit=12, tail='...'):
if len(text) <= limit:
return text
return text[:limit] + tail
print(truncate('Привет мир', limit=20))
1. Что делает normalize(' Привет МИР ') по умолчанию?
2. Как устроена word_count?
3. По чему sorted в top_words сортирует пары?
4. Зачем в счётчике написано counts.get(word, 0) + 1?
5. Что делают мини-проверки assert в проекте?
6. Слова три и кота встретились по два раза. Почему top_words поставил три первым?
7. Зачем в файле библиотеки пишут if __name__ == '__main__':?
8. Как устроена slugify из бонуса?
9. Чем библиотека утилит отличается от набора разрозненных функций?
Добавь в библиотеку утилиту char_count: она считает символы строки без пробелов. Напиши docstring и проверь на двух строках.
Как начать свою библиотеку функций на Python?
С одной утилиты: функция одной задачи с docstring и return, после неё — пара assert-проверок с известным ответом. Дальше добавляйте утилиты по одному шаблону и стройте крупные функции поверх готовых мелких, как text_report в проекте.
Урок 18: docstringЗачем утилитам docstring и значения по умолчанию?
Docstring отвечает на вопрос «что она делает» без чтения тела, а умолчание подбирает самый ходовой сценарий вызова: normalize чистит регистр без лишних аргументов, top_words по умолчанию возвращает тройку лидеров. Через месяц вы вызовете свою утилиту как чужую — визитка спасёт.
Что проверяют assert-проверки в проекте?
Известный вход и известный ответ: normalize(' A B ') должна дать 'a b'. Прошедшие проверки молчат, нарушенная падает с AssertionError и обрывает программу. Это проверки для разработки — с флагом -O Python их выбрасывает, поэтому для чужих данных нужен try/except.
Урок 19: ошибки функцийЧто учить после курса по функциям?
Курс ООП: классы собирают данные и функции в один тип, а ваши утилиты станут методами. Если чаще работаете с данными — курс Словари: счётчики и частотные таблицы проекта там становятся повседневным инструментом.
Курс ООП: 20 уроковНужно ли разносить библиотеку по файлам?
Пока код помещается на один экран — нет. Импорты, пакеты и порядок объявлений приходят вместе с ростом проекта, а не с пользой. Делите по темам, когда файл стал нечитаемым, а демонстрацию от библиотечной части отделите блоком if __name__ == '__main__':.
Как проверить, что библиотека работает правильно?
Assert-проверками после каждой утилиты: известный вход и известный ответ. Например, normalize(' A B ') должна дать 'a b'. Прошедшие проверки молчат, нарушенная обрывает программу с AssertionError — ломается громко и сразу.
Нужны ли библиотеке тесты, если есть assert-проверки?
Assert-проверки ловят поломку в момент, когда вы её внесли, — этого достаточно для учебного проекта. Полноценные тесты ставят на CI и пишут отдельно от кода; принцип тот же: проверка должна падать громко, как только поведение разошлось с ожиданием.
Понравился урок? Сошлитесь на него
«Библиотека утилит начинается с маленьких функций: каждая решает одну задачу, возвращает результат и несёт свою документацию.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Функции · Урок 4
Умолчания и именованные аргументы в Python
Необязательные параметры с запасными значениями и вызов по имени: def f(x, n=2) и f(x=5, n=3). Разбираем порядок параметров и знакомимся с ловушкой изменяемого умолчания.
Функции · Урок 9
sorted с key в Python: сортировка по правилу
Порядок по своему правилу: слова по длине, пары по цене, участники по очкам. Разбираем sorted, параметр key, reverse и стабильность Timsort.
Функции · Урок 18
docstring в Python: документация внутри функции
Документация живёт внутри функции: строка в тройных кавычках после def, атрибут __doc__ и честный формат короткого описания.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Словари · Урок 20
Проект: частотный словарь текста
Финальный проект: частотный словарь текста — режем текст на слова, чистим, отбрасываем стоп-слова, считаем через Counter и выводим топ-5 ровными колонками.
подсчёт слов pythonчастота слов python
Циклы · Урок 16
Циклы и строки в Python: разбор текста на слова
Разбор текста начинается со split: строка превращается в список слов, цикл считает, ищет и собирает отчёт.
цикл по строке pythonpython перебор слов в строке
Функции · Урок 1
Функции в Python: первая def
Первая функция: задаём блоку кода имя через def и вызываем по скобкам. Разбираем объявление на части — имя, двоеточие, тело с отступом — и видим, зачем функция вместо копипасты.
функции в pythonpython функции