Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Собрал все изменения в master и оформил статью #867

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
131 changes: 78 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Отделяйте ввод/вывод от обработки
# Ввод вывод VS Обработка данных


Ключевой критерий качества кода — это стоимость внесения в него изменений.
Expand All @@ -7,104 +7,129 @@
безгранично гибкий код — это как сферический конь в вакууме. Теоретически
возможен, но практической ценности не несет.

Если мы заранее будем знать как изменятся требования к коду через год, два или
десять лет, то уже сейчас можно заложить необходимые механизмы расширения
функциональности. К сожалению, такие сведения сложно получить, а зачастую
просто невозможно. Приходится опираться на свой опыт, опыт товарищей,
прорабатывать разные сценарии развития проекта, подстраховываться.

Один из часто встречающихся и оправданных приемов — это отделение обработки
данных от процесса ввода/вывода. Рассмотрим несколько примеров.

Пример. Подбор онлайн-курса
## Пример. Подбор онлайн-курса


По условию задачи нужно скачать из сети данные об онлайн-курсах, выбрать из
них лучшие и сохранить результат в xlsx файл. Вот фрагмент кода:

def get_courses_list(courses_url):
html = fetch_html(courses_url)
if html:
# .... parsing logic
return courses_list
else:
print("can't load list of courses")
```python
def get_courses_list(courses_url):
html = fetch_html(courses_url)
if html:
# .... parsing logic
return courses_list
else:
print("can't load list of courses")
exit()
```
Теперь примерим на себя роль провидца и подумаем какой функционал потребуется
через месяц:

В случае сетевой ошибки взять паузу в 10 секунд и повторить попытку, затем
1. В случае сетевой ошибки взять паузу в 10 секунд и повторить попытку, затем
подождать еще 30 секунд и так далее.
В случае если адрес недоступен - постучаться по другому url в зеркало сайта.
В случае ошибки сделать запись в лог и взять данные из ранее подготовленного
2. В случае если адрес недоступен - постучаться по другому url в зеркало сайта.
3. В случае ошибки сделать запись в лог и взять данные из ранее подготовленного
кеша.
Как все это сделать когда def get_courses_list сама завершает программу ?! От
вызова exit() надо отказаться. Можно выбросить исключение и таким образом

Как все это сделать когда `def get_courses_list` сама завершает программу ?! От
вызова `exit()` надо отказаться. Можно выбросить исключение и таким образом
сообщить о проблеме внешнему коду, пускай там разбираются.

Вызов print тоже стоит вынести из тела функции наружу. В рассмотренных
Вызов `print` тоже стоит вынести из тела функции наружу. В рассмотренных
сценариях вывод в консоль зависит от общей логики загрузки данных и
многократных вызовов def get_courses_list.
многократных вызовов `def get_courses_list`.

Что еще может потребоваться в скором будущем?

Отладить и покрыть тестами парсер HTML страницы.
Ускорить работу скрипта, хранить ранее скачанные страницы в кеше на жестком
1. Отладить и покрыть тестами парсер HTML страницы.
2. Ускорить работу скрипта, хранить ранее скачанные страницы в кеше на жестком
диске.
Ага, значит вызывать fetch_html() внутри def get_courses_list не такая уж
хорошая идея. Жить будет легче если передать в def get_courses_list строку с
HTML разметкой вместо courses_url. Вуаля, мы решили проблемы еще до их

Ага, значит вызывать `fetch_html()` внутри `def get_courses_list` не такая уж
хорошая идея. Жить будет легче если передать в `def get_courses_list` строку с
HTML разметкой вместо `courses_url`. Вуаля, мы решили проблемы еще до их
появления на горизонте!

Пойдем дальше. Код другой функции:

def get_course_info(html):
# ... parsing logic
```python
def get_course_info(html):
# ... parsing logic

rating = soup.find_all('div', attrs={'class': 'ratings-text'})
if rating: # check if rating is not empty list
rating = rating[0].contents[0].text
else:
# we wanna be user-friendly, with nice output to xlsx
rating = "No rating yet"
rating = soup.find_all('div', attrs={'class': 'ratings-text'})
if rating: # check if rating is not empty list
rating = rating[0].contents[0].text
else:
# we wanna be user-friendly, with nice output to xlsx
rating = "No rating yet"

# .... parsing logic
# .... parsing logic

return course_data
```
Что может произойти с кодом дальше?

Если рейтинга нет — надо искать его на другом сайте.
В xlsx указывать не просто отсутствие рейтинга, а еще на каких сайтах искал.
Отчет о курсах без рейтинга выгружать в дополнительную вкладку xlsx, чтобы
1. Если рейтинга нет — надо искать его на другом сайте.
2. В `xlsx` указывать не просто отсутствие рейтинга, а еще на каких сайтах искал.
3. Отчет о курсах без рейтинга выгружать в дополнительную вкладку `xlsx`, чтобы
удобнее было руками проверять.

Для всего этого нужно уметь отличать от прочих ситуацию "рейтинг неизвестен".
В Python для этих целей предусмотрено значение rating = None. А строку "No
rating yet" можно переместить туда где данные подготавливаются к выводу в xlsx.
В Python для этих целей предусмотрено значение `rating = None`. А строку "No
rating yet" можно переместить туда где данные подготавливаются к выводу в `xlsx`.

Та же функция, часть вторая, последняя:

def get_course_info(html):
# ... more parsing logic is here

# number prefix is usefull for simple sorting data before output to xlsx
return {
'1_title': title,
'2_date': start_date,
'3_language': language,
'4_weeks': duration,
"5_rating": rating
```python
def get_course_info(html):
# ... more parsing logic is here

# number prefix is usefull for simple sorting data before output to xlsx
return {
'1_title': title,
'2_date': start_date,
'3_language': language,
'4_weeks': duration,
"5_rating": rating
}
Сразу возникают вопросы. А если нужна еще одна выгрузка в формате csv, с
другим порядком столбцов, как это сделать? Как заменить столбец 2_date на
days_before_start ?
```
Сразу возникают вопросы. А если нужна еще одна выгрузка в формате `csv`, с
другим порядком столбцов, как это сделать? Как заменить столбец `2_date` на
`days_before_start`?

Кроме того, наперед известно, что пользовательский интерфейс — будь то вывод в
консоль или запись в файл — меняется очень часто. Было бы удобно собрать все,
что относится к форматированию вывода в одном месте. Например, всю логику
выгрузки в xlsx поместить в def fill_xlsx(workbook, courses):, а вывод в
консоль собрать внутри if __name__=='__main__':. Удастся избежать вычитывания
выгрузки в `xlsx` поместить в `def fill_xlsx(workbook, courses):`, а вывод в
консоль собрать внутри `if __name__=='__main__':`. Удастся избежать вычитывания
и повторной отладки всей программы от начала до конца, ведь изменения локальны
и изолированы.

Вместо заключения
## Вместо заключения


В результате мы пришли к ситуации, когда логика обработки данных слабо зависит:

1)от источника данных;
2)от формата вывода в файл.
1. от источника данных;
2. от формата вывода в файл.

![image](https://dvmn.org/filer/canonical/1594117412/678/)
=======
Кроме того, часть кода удалось превратить в [чистые функции](https://devman.org/encyclopedia/decomposition/decomposition_pure_functions/), что облегчит
тестирование и повторное использование.

Стратегия по отделению операций ввода/вывода от обработки данных встречается
повсеместно, в самых разных программах: от небольших скриптов до серьезных и
крупных проектов. Это один из базовых приемов, нужно уверенно им владеть.