Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 20 из 20 Начальный 25 мин 150 XP

Проект на функциях 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 покажет строку с точностью до символа — невидимые пробелы станут видимыми:

шаг 0: грязный текст крупным планом
raw = '  Кот пришёл  кот ушёл   кот вернулся  '
print(repr(raw))
print(raw.split())
Вывод
'  Кот пришёл  кот ушёл   кот вернулся  '
['Кот', 'пришёл', 'кот', 'ушёл', 'кот', 'вернулся']

repr показал пробелы по краям и двойные внутри, а split уже разобрал текст на слова: он режет по любым пробельным символам и игнорирует края. На этом и построятся первые две утилиты.

Кусок 1: normalize и word_count

Первая утилита чистит текст: схлопывает повторы пробелов в один и приводит к нижнему регистру. Регистр — по желанию: за это отвечает аргумент-флаг lower с умолчанием. Вторая утилита считает слова. Обе короткие, обе с docstring, а после демонстрации — мини-проверки:

кусок 1: чистка и счёт
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):
    ...
Фрагмент: многоточие вместо тела. Аргумент lower=True делает нормализацию регистра необязательной: вызов normalize(s) и normalize(s, 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 и срез:

кусок 2: частотный топ
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)
Фрагмент: sorted берёт пары «слово — счёт», лямбда указывает сортировать по pair[1] — по счёту, reverse=True — от большего к меньшему. Срез pairs[:n] оставляет первые n пар.

Утилита гибкая: один и тот же вызов с разными n отвечает на разные вопросы — полный расклад, пара лидеров, единственный чемпион. Умолчание n=3 подобрано под самый ходовой случай — топ-3.

Кусок 3: библиотека целиком

Куски 1 и 2 можно просто сложить в один файл — и получится библиотека. Дополнительная функция text_report не считает сама ни одного слова: она вызывает соседей. Так работает библиотека: функции строятся друг на друге, вместо повторения кода:

кусок 3: библиотека и отчёт
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))
Проверь себя
0 / 9

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. Чем библиотека утилит отличается от набора разрозненных функций?

Карточки терминов
Запомнено: 0 / 9
Практика

Добавь в библиотеку утилиту char_count: она считает символы строки без пробелов. Напиши docstring и проверь на двух строках.

practice.py
Вопросы и ответы по уроку

Как начать свою библиотеку функций на 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.