Откуда берутся нерешаемые проблемы

Во время программирования новички иногда попадают в тупик: “Я хочу сделать X, но код не работает!!!”. На самом деле с этой проблемой действительно ничего нельзя сделать. Проблему “Код не работает” невозможно решить.

Если в автосервис приедет машина, и автомеханик будет думать “Машина не работает, как починить машину?”, то он никогда её не починит. Автомеханики начинают исследовать автомобиль: что конкретно не работает? Машина не заводится или заводится, но не едет? А есть ли в машине бензин?.. Путём рассуждений они сужают проблему от “машина не работает” до вполне конкретных проблем, с которыми можно бороться:

  • В машине не работает двигатель, потому что … —> двигатель можно заменить
  • У машины пробито колесо —> можно его починить или заменить на новое
  • Водитель залил не тот бензин —> с этим можно справиться

Вы сталкиваетесь с проблемами, которые не можете решить, потому что недостаточно формализуете проблему. “Мой код не работает” — это не формализованная проблема. Нужно копнуть глубже, и понять, что это значит и почему он не работает. Код выбрасывает ошибку или запускается нормально, но делает не то, что нужно? Если он выбросил ошибку, то что в ней написано? На какой строчке ошибка? А что лежит в переменных на этой строчке? Как только вы начнёте задаваться такими вопросами, ставить гипотезы и их проверять, рано или поздно вы придёте к формализованным проблемам:

  • В коде не объявлена переменная —> объявлю её
  • Функция не возвращает значение —> поставлю return

Итого, формализация – это преобразование проблемы у вас в голове в подробный текст, с описанием причин этой проблемы и вариантов её решения.

Наверняка с вами так было: вы начали писать о своей проблеме коллеге или преподавателю, и вот тут-то до вас дошло решение! Это как раз потому, что вам пришлось её формализовать, чтобы другой человек вас понял.

Формализуйте проблему и у вас получится её решить

Есть ещё одна опасность при такой работе: не проверять то, во что вы верите. Представьте себе автомеханика, который произносит одно и то же для каждого клиента: “ну, раз машина не едет, значит, что-то с двигателем!”. Он заменяет всем двигатели и… И часть из них уходят довольными, а часть — потратили деньги и время впустую на замену двигателя, когда дело было совсем не в этом.

Не верьте в первую попавшуюся гипотезу

Чтобы разобраться в чём действительно дело нельзя хвататься за первое же объяснение вашей ошибки. Нужно выдвинуть несколько гипотез и проверить каждую. Иначе это слепая вера, а не отладка программы.

Как таблица помогает

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

spreadsheet sample

Заметили, что таблица выглядит странно и инородно, словно костыль? Она же тормозит ход мысли! Придётся без конца останавливаться, всё конспектировать и думать как разложить текст по ячейкам таблицы. Зачем всё это?!

Действительно, куда быстрее будет сделать то же самое, но без записей, а сразу у себя в голове. И опытные программисты так делают! Но раз вы здесь, значит, у вас не получилось. Примите это, вооружитесь таблицей. В этот раз она направит ваши поиски, а со временем вы так натренируетесь, что справитесь и без неё.

Жил был Ваня…

Ваня писал программу, которая скачивает вакансии с сайта hh.ru и показывает среднюю зарплату по языкам программирования. Он столкнулся с проблемой: не во всех вакансиях указана зарплата “от … и до …”. Где-то написано “от 40.000 рублей”, а где-то “до 400.000 рублей”.

Ваня решил так: пусть если указано только “от”, то я буду умножать зарплату на 1.2 и выдавать как результат. Если только “до”, то умножать буду на 0.8. А если указаны обе, то просто сложу и поделю на два.

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

def predict_salary(min_salary, max_salary):
    average_salary = 0
    if min_salary == None or 0:
        average_salary =  max_salary*0.8 
    if max_salary == None or 0:
        average_salary =  min_salary*1.2
    if (min_salary and max_salary) != None or 0:
        average_salary = ((max_salary+min_salary)/2)       
    return average_salary

Но вот беда: функция работает неправильно. Что делать? Вот несколько примеров, которые получились у Вани:

>>> min_salary, max_salary = 100, 200
>>> print(predict_salary(min_salary, max_salary))
150.0 #  Правильно, так Ваня и хотел: (100+200)/2=150

>>> min_salary, max_salary = 0, 100
>>> print(predict_salary(min_salary, max_salary))
50.0 # Ваня хотел получить 100*0.8=80, а получил 50

Даже если вы догадались в чём ошибка, это не важно. Вы всё равно можете пройти туториал ниже и потренироваться решать проблемы в будущем, это будет полезно.

1. Создайте таблицу по образцу

Мы подготовили для вас образец таблицы. Перейдите по ссылке и создайте копию, вот так:

copy-example

2. Сформулируйте проблему

Перед тем, как начать, стоит сформулировать проблему. Это первый шаг от “код Вани не работает” к чему-то конкретному. Должно получиться коротко и ясно, без терминов.

Несколько правил формализации:

  • Без общих слов, запрещены “не работает” или “сломалось”
  • Не слишком конкретно, без строк кода и прочего. Не-программист должен понять с чем боретесь и с какой проблемой столкнулись
  • Описывайте что видите, а не как это понимаете.

Несколько примеров, как получалось у других новичков:

  1. Делаю сайт с интерактивной картой, как Яндекс.Карты. Проблема возникла с командой, которая заполняет базу данных местами на карте, включая фотографии этих мест. Код выполняется без ошибок, но картинки почему-то не сохраняются в базе данных.
  2. Пишу чатбота-таймер. Хочу, чтобы он обновлял сообщение каждую секунду: “Осталось 5 секунд”, потом “Осталось 4 секунд” и так далее. Сообщение почему-то не обновляется, в консоль выводится ошибка: BadRequest: Message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message
  3. Пытаюсь отправить письмо по почте через smtplib, из питона. Залогиниться получилось, но письмо не отправляется: выскакивает ошибка о том, что письмо похоже на спам.

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

first-line-completed

3. Сформулируйте противоречие

Противоречие — это “я ожидал одно, а получилось другое”. Это следующий шажок в сторону решения проблемы. Противоречие уже ближе к коду, тут можно использовать термины и указывать на строчки кода.

Ощущение от противоречия — удивление. Вы писали код, написали строчку, запустили, а он сделал совсем не то, что вы ожидали. Вспомните, в какой момент это случилось.

Примеры противоречия:

  1. Я написал строчку image.save(), а картинка не сохранилась

Действительно, почему так? Вроде должна была сохраниться!

  1. Функция жалуется, что я не передаю аргумент, а я вроде передал

Неужели питон неправильно работает? Что за дела?

  1. Отправляю письмо по почте через smtplib, но оно помечено как спам. Хотя в тексте письма только “Привет”.

Действительно, простое приветствие на спам не похоже. В чём может быть дело?

Противоречия возникают от того, что вы в своей голове читаете код не так же, как Python. Он перевёл код по-своему, а вы по-своему. Чтобы избавиться от ошибки, нужно найти то место, где вы переводите код на русский неправильно.

Попробуйте сформулировать противоречие к коду Вани. В каком месте в коде он сделал что-то, что питон понял неправильно? Опишите это текстом, в таблице, которую вы создали. Постарайтесь чтобы получилось примерно как в примерах выше.

Если вы даже не можете найти примерное место, из-за которого возникает ошибка, то это тоже может быть противоречием: “Код вроде правильный, но где-то ошибка. Где?”

4. Гипотезы

Теперь, когда вы определились в чём конкретно у вас проблема, нужно предложить несколько гипотез для её решения или объяснения. Гипотеза — это предположение о том, что могло привести к противоречию.

Смотрите не в код, а на противоречие. Что могло привести к этому? Чтобы придумать хорошую гипотезу, даже не нужно видеть код.

Вот проблема и противоречие, которое мы сформулировали для Вани:

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

Противоречие: Функция возвращает неправильный ответ, если указано max_salary=40000 и min_salary=0. Ожидаю 32.000, выводится 20.000.

А вот и несколько гипотез для этого противоречия:

  • Первое условие вообще не срабатывает
  • Первое условие не срабатывает с данными 40тыс. и 0
  • Условие срабатывает, но вместе с ним сработало и последнее
  • Вызывается вообще не эта функция, запускается другая, я что-то перепутал в коде
  • Условие срабатывает правильно, но код внутри if работает неправильно
  • Может, функция работает правильно, а я обсчитался?

Некоторые из этих гипотез правдоподобные, а в некоторые сложно поверить. Но всё же, не хватайтесь за первую попавшуюся. Иногда кажется, что вот оно, решение. Но это ловушка вашего мозга. Его заставляют формализовывать мысли, а ему это не нравится. Он будет пытаться отлынивать, как-нибудь закрыть таблицу. Он захочет открыть код и поскорее попробовать пошаманить: попереставлять строки местами, стереть какие-нибудь строки или добавить больше запятых. Не поддавайтесь. Таблица давит в ваше больное место, мозг хочет это прекратить. Но нужно его пересилить, иначе проблему вам не решить.

Сформулируйте хотя бы 3 гипотезы, прежде чем открывать код

Несколько примеров плохо сформулированных гипотез:

  • Я написал неправильный код

Такую гипотезу невозможно проверить, от неё нет никакого толку

  • Если поменять условия местами, то всё исправится

Это не гипотеза. Она не объясняет почему код не работает, это просто предположение чего бы такого сделать. В поиске ошибки это не поможет.

  • Может, hh.ru отдаёт неправильные данные?

Эта гипотеза к проблеме не относится. Мы проверяем как работает функция, никакого кода про hh.ru здесь нет.

А вот как это выглядит в таблице:

5. Как проверить гипотезы

Теперь, когда у вас есть гипотезы, осталось их проверить.

Примеры плохой проверки:

  • Первое условие вообще не срабатывает —> перечитаю код

Вы его уже читали. Чтение кода — не гарант того, что что-то работает или нет. Проверьте вручную: запустите этот код.

  • Условие срабатывает, но вместе с ним сработало и последнее —> поменяю их местами

Если вы поменяете их местами, получите какой-то результат, то как вы поймёте, в чём было дело? Как не допустить эту ошибку снова?

  • Условие срабатывает правильно, но код внутри if работает неправильно —> спрошу у коллег, все-ли делали так же

Это просто отказ от решения проблемы. Вы просто хотите, чтобы её решили за вас. Нужно учиться решать проблемы самостоятельно. Без этого навыка программистом вы не станете.

Примеры хорошей проверки гипотез:

  • Первое условие вообще не срабатывает —> Поставлю в него print('test'). Если выведется в консоль, то срабатывает.
  • Условие срабатывает, но вместе с ним сработало и последнее —> Поставлю в первое print('test1'), а во второе print('test2'). Если выведутся оба, значит правда.
  • Условие срабатывает правильно, но код внутри if работает неправильно —> Запущу код внутри условия отдельно. Должен из 0 и 40.000 сделать 32.000.

Заполните оставшиеся ячейки в таблице Вани самостоятельно. Нужно заполнить все столбцы справа от каждой из гипотез:

6. Что дальше?

По итогу проверки гипотез оказалось, что одна из гипотез верна, а остальные — нет.

У Ивана возникло новое ощущение удивления: почему так? Он формулирует из него противоречие.

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

При работе по таблице иногда такое бывает, что для решения проблемы этот цикл надо повторить 4-5 раз. Зато в процессе вы узнаете много нового и решите проблему самостоятельно!

Написание кода — это циклический процесс. Никакие программы не пишутся с первой попытки. Их пишут в несколько заходов, постоянно возвращаясь назад и что-то изменяя/дописывая. Это нормально.

Почему проблема решится

Может ли быть такое, что вы будете бесконечно выписывать проблемы и гипотезы? или что они зациклятся?

Ответ: нет. Каждая из проверенных гипотез сокращает ваше незнание о проблеме. Сначала вы боролись с проблемой в файле. Через несколько гипотез узнаете на какой строчке ваша проблема. Если этого недостаточно — узнаете на какой части этой строчки. Дальше вы узнаёте почему эта строчка не работает, пробуете её на что-нибудь заменить. Вы всегда в движении на пути решения проблемы.

Если вы проделали всё выше со своей проблемой и всё равно не получается — пришлите свою таблицу преподавателю. Может, вы не догадались до какой-то из гипотез? Он предложит вам новых, и вы сможете дорешать проблему сами. Это куда круче, чем просто задать ему вопрос “У меня не работает код, помогите”. Вы проделали бОльшую часть по решению проблемы сами, и от преподавателя понадобится уже совсем крошечное участие.

Полезные ссылки


Попробуйте бесплатные уроки по Python

Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.

Переходите на страницу учебных модулей «Девмана» и выбирайте тему.