Ответ на пост «Динамика ключевых слов в Гарри Поттер и Орден Феникса»
Я пообещал @rick1177, что напишу этот пост.
Я расскажу, почему я считаю отвратительным программный код из видео из поста «Динамика ключевых слов в Гарри Поттер и Орден Феникса». Но я не программист, просто увлекаюсь. Не-программистам дальше читать будет скучно, листайте дальше.
Во-первых, код написан в стандартном Блокноте, т.е. программе, которая вроде бы и способна, но не предназначена для написания кода. Если @programming.fun смог установить в систему интерпретатор Python, что мешает установить хотя бы примитивный Notepad++? Вполне возможно, чтобы не было видно номеров строк, и таким образом усложнить критику таким, как я. А может, и нет. В любом случае взрослые дядьки пишут в навороченных IDE не просто так - специализированные редакторы и среды разработки сильно помогают в процессе написания кода и его дебаге.
Во-вторых, не думал, что кто-то еще пользуется графическим REPL (я уже забыл, как оно там правильно называется, не суть). Ну и бох с ним — если удобно, почему бы и не пользоваться? Я же живу и работаю под Linux, поэтому и запускать код я буду в консоли.
Для того чтобы провести бесплатный код-ревью, мне потребовалось вручную набрать код по видео, и найти исходную книгу. Код я набрал, проверил два раза, вроде бы все буква в букву. В ответ на просьбу дать исходную книгу @programming.fun предложил погуглить самому. Ну что ж, что нагуглилось, то нагуглилось, хоть и наши с ним результаты не совпадают совсем. Ничего страшного, дело не в результатах, а в работе кода.
Просто так этот код не заработает. Нужно подготовить для него среду выполнения. По уму это делается через штуки вроде Pipenv или Poetry, последний я начал использовать совсем недавно. Программы такого рода решают две задачи: управление зависимостями (а-ля requirements.txt) проекта и создание среды выполнения (virtualenv) для проекта. Не хочу растекаться мыслью по древу и объяснять, как со всем этим работать — каждый программист просто обязан это знать как можно раньше.
Я выполняю команду poetry init, отвечаю на вопросы и базовая настройка завершена. Далее я выполняю по одной команде на каждую зависимость кода:
poetry add pandas
poetry add numpy
poetry add bar-chart-race
poetry add nltk
И все зависимости для проекта установлены. Если сразу запустить код, nltk будет ругаться, что не скачан корпус "stopwords", для этого надо выполнить еще и команду
python -m nltk.downloader stopwords
Я опубликовал репозитарий с кодом здесь. Каждый коммит — одно мое исправление. Я пронумеровываю их, чтобы было проще следить за ними по мере чтения этого поста.
В самом начале время выполнения кода на моем ноутбуке-старичке заняло 1706.4012822469958 секунд, то есть более 28 минут.
Далее я буду использовать диапазоны строк вроде 13-34. Это значит, что, например, я говорю про строчки с 13 до 34 включительно.
Коммит 1
Давайте будем отделять текст комментария от символа # пробелом, а? Об этом говорится здесь. И уберем двоеточия в конце каждого комментария, потому что это избыточно. Про грамматику английского я уже говорить не буду, а то предъявят, что я просто придираюсь. Но английский знать надо.
Коммит 2
Первое, что бросается в глаза — 13 строчка с file.close. Она по идее нужна, чтобы закрыть файловый дескриптор после того, как мы поработали с файлом. Но для интерпретатора это выражение, которое ничего не делает! Автор просто забыл добавить скобки для вызова метода. Давайте их добавим!
Коммит 3
В итоге у нас получается конструкция в строках 11-13, в которой мы открываем файл, читаем его содержимое (очень странным образом) и закрываем его. Для этой цели нужно использовать так называемые контекстные менеджеры, чтобы не закрывать дескриптор файла вручную. Перепишем это на:
with open("Book 5 - The Order of the Phoenix.txt", "r", encoding='utf-8') as file:
file_rows = file.readlines()
и количество строк сразу уменьшилось до 2, заодно этот кусок кода стал более идеоматичным.
Коммит 4
Взглянув на код шире, понимаем, что странного в чтении файла — мы сначала читаем строки, а потом объединяем их в строку.
Это плохо потому, что в Python строки это неизменяемая структура данных. Что это значит? В памяти создается пустая строка. Затем в цикле мы к этой строке добавляем другие строки. Для интерпретатора это выглядит как "создать пустую строку, для каждого i из file_rows вычислить i + file_text, затем удалить старую file_text, и на ее место установить новую file_text". Потому что к строке нельзя просто добавить новую строку. Интерпретатор вычисляет новую строку, и устанавливает ее в памяти на место старой. Такие действия крайне неоптимизированны.
Решение? Использовать метод join у строчного типа, который принимает коллекцию элементов, и сшивает их в одну строку:
file_text = "".join(file_rows)
Еще 3 строки исчезли за ненадобностью.
Коммит 5
Взгляните на строки 11-15. Все эти махинации мы проделываем только для того, чтобы получить переменную file_text с текстом книги. Что мешает сразу прочитать весь файл в эту переменную, вообще не жонглируя со строками? Вместо метода readlines() надо использовать read(), только и всего. Еще 3 строки ушли в небытие.
Коммит 6
Теперь в 25 строке видно, что автор забыл удалить пустые строки из массива. Зачем они? В чем их предназначение? Резать к чертовой матери.
Коммит 7
Строки 22-30 нужны для того, чтобы очистить текст от определенных символов. Для этой цели существуют регулярные выражения. Согласен, они сложны и неинтуитивны, но когда постигнешь их суть, твой мир уже не будет прежним. Импортируем встроенную библиотеку re и переписываем. Для цифр вообще есть регулярка "\d+", которая означает "любое количество идущих подряд цифр, от 1 до бесконечности", поэтому нет необходимости гонять поиск-замену в цикле 10 раз, регулярка делает это за 1 проход.
Коммит 8
Назначение 31-32 я понимаю, как раздробление всего текста книги на части, каждая из которых — отдельная страница. Зачем сначала гонять интерпретатор по всему тексту, заменяя вхождения "page |" на символ собаки, а потом еще раз гонять интерпретатор по всему тексту, чтобы разбить его по символу собаки на куски? Почему нельзя это сделать за один проход?
Коммит 9
А теперь для каждого куска текста мы будем считать слова... И судя по строкам 34-36, списковые выражения мы использовать не умеем или упорно не хотим... Исправляем.
Коммит 10
От строк 36-41 у меня кровоточат глаза. Однобуквенные переменные, явное задание ноля в range()... Исправлю, может, потом будет понятнее, что тут творится. Заодно уберу начальный ноль во всех range(), потому что range() и так генерирует последовательность с ноля.
Коммит 11
В строках 47-48 вместо итерации по range определенной длины можно использовать простую итерацию по последовательности.
Вы знаете, что, ребята... Я устал. Я больше не хочу в этом участвовать. Редактировать чужой код это действительно трудозатратно. Думаю, 11 моих исправлений хотя бы покажут, что тупой набор кода это тоже искусство, и к этому надо отнестись ответственно. В коде еще много проблем, но мне теперь кажется, что проще переписать все начисто. Возможно, когда-нибудь. Прикольная эта библиотека, bar_chart_race...