Книга: «Программирование на Python с помощью GitHub Copilot и ChatGPT.»

МЕНЮ


Главная страница
Поиск
Регистрация на сайте
Помощь проекту
Архив новостей

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


2024-08-11 15:02

разработка по

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

Узнайте, как создавать и улучшать программы на Python с помощью ИИ, даже если прежде вы не написали ни строчки компьютерного кода. Сэкономьте время на рутинном программировании и воспользуйтесь услугами ИИ, способного мгновенно воплотить ваши идеи в жизнь. Заодно выучите Python!

Проектирование функций

В этой главе

  • Функции в Python и их роль в проектировании ПО.
  • Задачи, которые должен решать Copilot.
  • Стандартный рабочий процесс при взаимодействии с Copilot.
  • Примеры написания хороших функций с помощью Copilot.
Одна из самых больших сложностей для начинающих программистов — понять, какую задачу задать Copilot, чтобы он нашел хорошее решение. Если дать ему слишком большую и трудную задачу, то он часто будет терпеть неудачу и давать неверный ответ, который будет очень сложно исправить. Что же тогда является подходящей задачей?

Этот вопрос важен для использования Copilot, но выходит далеко за рамки этой книги. Люди тоже постоянно борются со сложностями. Если опытные инженеры-программисты пытаются написать код для решения слишком сложной задачи, не разбивая ее на мелкие, более просто решаемые части, то у них тоже часто возникают проблемы. Решением для людей стало использование функции, которая выполняет только одну задачу. Существуют различные правила, как написать такую функцию с точки зрения количества строк кода, но в основном речь в них идет о написании чего-то, что: 1) выполняет одну задачу; 2) не является настолько сложным, что это было бы трудно корректировать.

Студенты, которые учились программировать по старинке, без Copilot, около месяца изучали синтаксис в коде длиной 5–10 строк, прежде чем мы начинали знакомить их с функциями. В этот момент вполне естественно сразу сказать им не писать в одной функции больше кода, чем они смогут протестировать и отладить. Поскольку вы учитесь работать с Copilot, а не с синтаксисом напрямую, наша задача в этой главе состоит в том, чтобы рассказать вам о функциях и о том, какие задачи разумно и неразумно решать, используя Copilot в рамках одной функции.

Чтобы помочь вам получить представление о функциях, мы приведем несколько примеров. В них мы будем использовать центральный рабочий процесс взаимодействия с Copilot: цикл написания запросов, получения кода от Copilot и проверки его правильности. В функциях, создаваемых Copilot, вы начнете видеть основные инструменты программирования, такие как циклы, условия и списки, которые мы рассмотрим в следующих двух главах.

ФУНКЦИИ

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

Предположим, вы нашли в газете головоломку для поиска слов, которую хотели бы решить (рис. 3.1). В таких головоломках вы ищете каждое слово в списке слов. Они могут быть расположены слева направо, справа налево, сверху вниз или снизу вверх.

image
Рис. 3.1. Пример с поиском слова — поисковая головоломка

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

Попробуйте прямо сейчас поработать над этой задачей пару минут. Как бы вы начали? Как бы вы разбили задачу на части, чтобы сделать ее более выполнимой?

Вы можете сказать: «Хорошо, найти все слова — большая задача, но меньшая задача — найти первое слово (CAT). Сначала я поработаю над этим!» Это пример того, как можно взять большую задачу и разбить ее на более мелкие. Чтобы решить всю головоломку, вы можете повторить эту небольшую задачу для каждого слова, которое вам нужно найти.

Как же найти отдельное слово, например CAT? Даже эту задачу можно разбить, чтобы облегчить ее решение. Например, можно разделить ее на четыре подзадачи: поиск CAT слева направо, поиск CAT справа налево, поиск CAT сверху вниз и поиск CAT снизу вверх. Мы не только ставим перед собой все более и более простые задачи, но и делим нашу работу на отдельные логические части. И что самое важное, это более простые задачи, код которых мы попросим Copilot написать и в конечном итоге соединить в наши полноценные программы.

Решение большой задачи путем ее разделения на более мелкие называется декомпозицией. Это один из самых важных навыков в разработке программного обеспечения. Мы посвятили ему главу 7. Пока же вам важно понимать, когда задача слишком велика, чтобы просить Copilot выполнить ее. Просьба сделать новую видеоигру, которая представляет собой сочетание Wordscapes и Wordle, будет безуспешной. Однако вы можете дать Copilot указание написать функцию, которая позволяет решить более крупную задачу; например, у вас может быть часть кода, проверяющая, есть ли указанное игроком слово в списке допустимых слов. Copilot может хорошо выполнить данную часть, и это поможет ему приблизиться к решению более крупной задачи.

Компоненты функции

Происхождение названия «функция» восходит к математике, где функции определяют выход чего-либо на основе входных данных. Например, если , то мы можем сказать, что, когда x равно 6, f(x) будет 36. В программировании функции тоже имеют ожидаемый вывод для определенных входных данных, так что это название вполне подходит и для программирования.

Кроме того, будучи инженерами-программистами, мы любим думать о функциях как о промисах или контрактах. Если есть функция «больше» и нам говорят, что она берет два числа и выдает большее из двух, то мы верим, что, когда дадим функции числа 2 и 5, она вернет ответ 5. Нам не нужно видеть, как она работает, чтобы использовать ее, так же, как нам не нужно знать принципы работы механики автомобиля, чтобы использовать педаль тормоза. Нажимаем тормоз — и машина замедляется. Дайте функции два числа — и она вернет большее из них.

Каждая функция в Python имеет заголовок (также называемый сигнатурой), которым является первая строка ее кода. Учитывая их повсеместную распространенность, мы захотим читать и записывать заголовки функций. Заголовок функции описывает имя функции и все ее входные данные. В некоторых других языках он иногда содержит информацию о том, как выглядит вывод, но в Python ее нужно искать в другом месте кода.

В главе 2 мы писали комментарии, чтобы указать Copilot, что делать. Мы можем продолжать использовать этот подход, если хотим, чтобы Copilot сгенерировал нужную функцию. Например, с помощью комментариев попросить Copilot написать функцию, которая скажет, какое из двух чисел больше:

image
Как и в случае с кодом в прошлой главе, мы просто написали комментарии, чтобы побудить Copilot выдать нам код. Заголовок функции состоит из трех основных компонентов: ключевого слова, которое сообщает Python, что это функция, ее имени и входных данных для нее. В конце строки также есть двоеточие — обязательно поставьте его, иначе код не будет корректным кодом Python. Слово def обозначает, что создается именно функция. После def идет имя функции; оно должно описывать ее поведение как можно лучше. Имя этой функции — larger. Если трудно назвать функцию, поскольку она выполняет много разных действий, то обычно это признак того, что задача слишком большая для одной функции и ее надо декомпозировать, но об этом поговорим позже. То, что указывается в скобках при объявлении функции, — параметры. С их помощью вы предоставляете функции информацию, необходимую для ее работы. Она может иметь любое количество параметров, а у некоторых функций их не бывает вообще. Наша функция имеет два параметра — num1 и num2; два потому, что ей нужно знать оба числа, которые она сравнивает.
У функции может быть только один вывод; ключевое слово, которое следует искать при определении того, что она выводит, — это return. Все, что идет за return, представляет собой вывод. В этом коде будет возвращено либо num1, либо num2. Функции не обязаны что-либо возвращать (например, функция, которая выводит список на экран, не имеет причин возвращать некие данные), так что, если вы не видите оператор возврата, это не обязательно проблема, поскольку функция может делать что-то еще (взаимодействовать с пользователем), а не возвращать данные. Кроме того, функции должны либо возвращать что-то, либо не возвращать: они не могут возвращать что-то в одних случаях и ничего в других.

Мы попросили Copilot сгенерировать эту функцию с помощью комментариев, однако на самом деле такой подход усложняет его работу. Сначала Copilot должен правильно составить заголовок, в том числе выяснить, сколько параметров вам нужно получить. Затем он должен верно создать рабочий код функции.

Существует альтернативный способ: предложить Copilot написать код такой функции, который может помочь ему сгенерировать код наболее точно и нам лучше понять, что именно она должна сделать. Для этого нужно написать строки документации, и мы будем использовать их для написания функций на протяжении большей части книги.

СТРОКИ ДОКУМЕНТАЦИИ ОБЪЯСНЯЮТ ПОВЕДЕНИЕ ФУНКЦИЙ

Строки документации — это способ описания функций Python программистами. Они следуют за заголовками функции и начинаются и заканчиваются тремя двойными кавычками (""").

Написав заголовок и строки документации, вы облегчите Copilot генерацию правильного кода. В заголовке вы сами определяете имя функции и указываете имена всех параметров, которые должны использоваться. После заголовка функции вы приводите строку документации, которая расскажет Copilot, что делает эта функция. Затем, как и раньше, Copilot сгенерирует код. Поскольку мы дали Copilot заголовок функции, он сможет учиться на нем, что уменьшит вероятность совершения ошибок.

Вот как выглядел бы альтернативный подход при написании той же функции larger:

image
Обратите внимание, что мы написали заголовок функции, а также строки документации, а Copilot предоставил тело функции.

Использование функции

Как использовать имеющуюся функцию? Вспомним аналогию с . Как дать функции значение 6 для x, чтобы она вернула 36? Посмотрим, как это сделать с помощью кода, используя функцию larger, которую мы только что написали.

Использовать функцию можно путем ее вызова. Вызвать функцию — значит вызвать ее при определенных значениях параметров. Они называются аргументами. Каждое значение в Python имеет тип, и мы должны следить за тем, чтобы задавать значения соответствующего типа. Например, функция larger ожидает два числа; она может не сработать ожидаемым образом, если мы введем не числа. Когда мы вызываем функцию, она выполняет свой код и возвращает результат. Нам нужно зафиксировать его, чтобы использовать позже; в противном случае он будет потерян. Чтобы зафиксировать результат, мы используем переменную, которая представляет собой просто имя, ссылающееся на значение.

В этом коде мы просим Copilot вызвать функцию, сохранить результат в виде переменной, а затем вывести его на экран:

image
Код правильно вызывает larger. Обратите внимание, что он помещает два сравниваемых значения после открывающей круглой скобки. Завершая работу, функция возвращает значение, которое мы присваиваем результату. Затем выводим результат на экран. Если вы запустите эту программу, то увидите, что на выходе получается 5, и это потому, что 5 — большее из двух значений, о которых мы спрашивали. Ничего страшного, если вы не разобрались во всех нюансах. Мы хотим, чтобы вы поняли, что, когда вызывается функция, как здесь:
larger(3, 5)

общий формат вызова функции выглядит так:

function_name(argument1, argument2, argument3,... )

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

ПРЕИМУЩЕСТВА ФУНКЦИЙ

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

  • Когнитивная нагрузка. Возможно, вы уже слышали о ней. Это количество информации, которую ваш мозг может обработать в любой момент времени и при этом продолжать работать эффективно. Если вам дадут четыре случайных слова и попросят повторить их, то вы, возможно, сможете это сделать. Если же слов будет 20, то большинство из нас не справятся, поскольку этот объем информации слишком велик. Аналогично, если вы когда-нибудь отправлялись в путешествие с семьей и пытались оптимизировать время в пути, учитывая остановки для детей, перерывы на обед, остановки в туалете, заправки, удачное расположение отелей и т. д., вы, возможно, почувствовали, что голова идет кругом из-за необходимости управлять всеми этими факторами одновременно. Когда вы не можете справиться со всеми условиями — это как раз и означает, что вы превысили вычислительные возможности своего мозга. Программисты сталкиваются с той же проблемой. Если они пытаются сделать слишком много за один раз или решить слишком сложную задачу в одном фрагменте кода, им трудно сделать это правильно. Функции призваны помочь программистам избежать слишком большого объема работы.
  • Избегание повторений. Программисты (как и все люди) не очень-то любят решать одну и ту же задачу снова и снова. Если мы напишем функцию, которая может правильно вычислить площадь круга, то нам не нужно будет писать этот код больше никогда. Это значит, что если наш код состоит из двух частей, в которых нужно вычислить площадь круга, то мы напишем одну функцию, которая вычислит площадь круга, а затем попросим свой код вызвать эту функцию в каждом из этих двух мест.
  • Улучшение тестирования. Гораздо сложнее протестировать фрагмент кода, который выполняет несколько действий, по сравнению с тем, который выполняет одну задачу. Программисты используют различные техники тестирования, но ключевая известна как модульное тестирование. Каждая функция принимает некие входные параметры и дает что-то на выходе. Например, для функции, которая вычисляет площадь круга, на входе будет радиус круга, а на выходе — его площадь. Модульные тесты дают функции входные данные, а затем сравнивают их с желаемым результатом. Что касается функции площади круга, то мы можем протестировать ее, задавая различные входные данные (например, несколько маленьких положительных чисел, несколько больших положительных чисел и 0), и сравнить полученный результат с известными нам правильными значениями. Если ответы функции совпадают с нашими ожиданиями, то мы можем быть более уверены в том, что код правильный. Если он выдает ошибку, то нам не придется проверять его слишком долго, чтобы найти и устранить проблему. Но если функция выполняет несколько задач, это значительно усложняет процесс тестирования, поскольку вам необходимо проверять каждую задачу и их взаимодействие.
  • Повышение надежности. Когда мы пишем код как опытные инженеры-программисты, то знаем, что допускаем ошибки. Мы знаем и то, что Copilot тоже может их сделать. Если представить, что вы замечательный программист и каждая написанная вами строка кода с вероятностью 95 % будет правильной, как вы думаете, сколько строк кода вы сможете написать, прежде чем хотя бы одна из них с вероятностью 95 % окажется неверной? Ответ — всего 14. Мы считаем, что 95 % корректности на строку — это большое достижение даже для опытных программистов и, скорее всего, более высокое, чем то, которое может выдать Copilot. Сохраняя задачи небольшими, решаемыми, в пределах 12–20 строк кода, мы уменьшаем вероятность того, что в коде есть ошибка. Хорошо выполнив тестирование, о котором мы упоминали чуть выше, мы можем быть еще более уверенными в правильности кода. Наконец, нет ничего хуже, чем код, содержащий несколько ошибок, которые взаимодействуют друг с другом, а вероятность множественных ошибок растет тем выше, чем больше кода вы пишете. Мы оба участвовали в многочасовых отладочных процедурах из-за того, что в нашем коде было несколько ошибок, и в результате стали гораздо лучше тестировать короткие фрагменты кода!
  • Улучшение читабельности кода. В этой книге с помощью Copilot мы будем в основном писать код с нуля, но это не единственный способ его применения. Если у вас есть более крупный фрагмент программы, которую вы или ваши коллеги редактируете и используете, то Copilot может написать код и для нее. Понимать код — в интересах каждого программиста, независимо от того, написан он людьми или Copilot. Это позволяет легче находить ошибки, определять, какой код нужно начинать изменять, когда мы хотим добавить новые функции, и иметь краткое представление о том, что будет легко или трудно реализовать в общем дизайне программы. Разбивка задач на функции помогает понять, что делает каждая часть кода и как они взаимодействуют. Вдобавок это помогает разделить работу и ответственность за правильность кода.
Эти преимущества очень важны для программистов. В языках программирования не всегда были функции. Но даже до того, как они появились, программисты делали все возможное, чтобы использовать другие инструменты для имитации функций. Эти приемы были не слишком хороши (если интересно, погуглите «операторы goto»), и все программисты счастливы, что теперь у нас есть настоящие функции.

Вы можете спросить: «Я понимаю, что эти преимущества важны для людей, но как они влияют на Copilot?» В целом мы считаем, что все принципы, применимые к людям, применимы и к Copilot, хотя иногда причины этого совсем иные. Copilot может не испытывать когнитивной нагрузки, но будет работать лучше, если мы попросим его решить задачи, аналогичные тем, которые уже решались людьми раньше. Поскольку люди пишут функции для решения задач, Copilot будет подражать им и тоже писать функции. После того как мы написали и протестировали функцию, вручную или с помощью Copilot, мы не захотим писать ее снова. Знать, как проверить работу программы, важно независимо от того, кто написал код: человек или Copilot. Он тоже может совершать ошибки при создании кода, поэтому мы хотим находить их быстро, как и в случае с кодом, написанным человеком. Даже если вы работаете только над собственным кодом и никогда никого не просите читать его, мы, как программисты, которым приходилось редактировать свой код, написанный много лет назад, должны сказать: очень важно, чтобы ваш код был читаемым, даже если единственный человек, читающий его, — это вы.

РОЛИ ФУНКЦИЙ

Функции играют в программировании самые разные роли. Если коротко, то программы — это функции, которые часто вызывают другие функции. Важно отметить, что все программы, в том числе написанные на Python, начинаются с одной функции (называемой main в таких языках, как Java, C и C++). Функция main в Python — это, по сути, первая строка кода, которая не находится внутри функции. Но если каждая программа начинается с одной функции, а мы только что сказали вам, что пытаться решить большую задачу с помощью одной функции — это ошибка, то как же это работает? Ну, main будет вызывать другие функции, которые, в свою очередь, будут вызывать другие и т. д. Код по-прежнему будет выполняться (в основном) последовательно в каждой функции, поэтому может начинаться с main, но затем переходить к другой функции и т. д.

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

Листинг 3.1. Код Python для демонстрации того, как Python обрабатывает вызовы функций

image
Если бы мы запустили эту программу, то результат был бы таким (ниже мы объясним почему):
Hi there my friend .  I'm well .  Bye.

На рис. 3.2 представлена схема того, как код из листинга 3.1 будет выполняться компьютером. Мы намеренно привели пример, содержащий множество вызовов функций, чтобы связать воедино то, что вы узнали только что. Опять же это не практический код; он приведен только в учебных целях. Давайте проследим за его выполнением. Возможно, будет проще обратиться к рис. 3.2, чем к листингу 3.1, но любой из вариантов подойдет.

image
Рис. 3.2. Последовательность выполнения функции в нашем примере из листинга 3.1

Программа начнет выполнение с первой строки в коде Python, которая не является функцией (print(«Hi»)). Хотя в Python нет функции main как таковой, мы будем называть блок кода после функций main, чтобы облегчить объяснение. Код выполняется последовательно, если только не встречает команды, которые дают ему указание выполнить код в другом месте. Так, после выполнения print(«Hi») он перейдет к следующей строке, которая является вызовом функции funct1: funct1(). Вызов funct1 меняет место выполнения кода на начало этой функции, которое является оператором: print(«there»). Следующая строка funct1 вызывает funct2, поэтому программа выполнит первую строку funct2: print(«my»). Что интересно, по завершении работы funct2 больше нет строк кода для выполнения, поэтому программа автоматически возвращается к первой строке, следующей за вызовом funct2 в funct1. (Если вызов функции находится в середине другого оператора, то этот оператор возобновит выполнение, но в данном примере вызовы функций находятся каждый на отдельной строке.) Вам может быть интересно, почему код переходит на следующую строку после вызова funct2, а не возвращается к вызову funct2. Проблема в том, что если бы он вернулся к вызову funct2, то навсегда бы застрял в этом вызове. Как результат, функции всегда возвращаются к следующему фрагменту кода (в данном примере — к следующей строке) после их вызова.

Продолжим пример: следующей выполненной строкой кода будет строка, которая выводит на экран friend. Очередная строка вызывает функцию funct3, которая выводит точку (.) и возвращается обратно к источнику вызова.

Итак, мы вернулись в funct1, в строку print(""). Вывод на экран пустого фрагмента текста приводит к появлению новой строки. Теперь funct1 завершена, поэтому передает выполнение обратно в следующую строку main после ее вызова. Мы подозреваем, что вы уже поняли, о чем идет речь, поэтому немного ускоримся:
  1. далее main выводит на экран I'm, а затем вызывает funct4;
  2. funct4 выводит well, а затем возвращается в main, где в следующей строке кода вызывает funct3;
  3. funct3 выводит точку (.) и возвращается в main. Обратите внимание, что funct3 была вызвана и funct1, и main, но это нормально, поскольку функции помнят, как вернуться в точку вызова. На самом деле наличие нескольких элементов, вызывающих одну и ту же функцию, — признак того, что такая функция является хорошей, поскольку используется повторно;
  4. после того как funct3 вернется в main, она выведет "", что приведет к началу новой строки, а затем выведет слово Bye.
Это был длинный пример, но мы привели его, чтобы дать вам представление о том, как выполняются функции и как программы состоят из их определения и вызова. Любое ПО выполняет конкретные задачи: вероятно, программисты написали одну или несколько функций для каждой из них. Кнопка в текстовом редакторе, которая меняет шрифт на жирный, вероятно, вызывает функцию для изменения шрифта. Эта функция также может изменить внутреннее представление редактора о тексте (редактор, скорее всего, сохранит ваш текст не в том формате, в котором вы его видите), а затем может вызвать другую функцию, которая обновляет пользовательское (то есть ваше) представление о тексте.

С помощью этого примера мы бы хотели также обсудить различные роли, которые играют функции. Задача вспомогательной функции — облегчить работу другой функции. В каком-то смысле каждая из функций, которая не является main, представляет собой вспомогательную.

Одни функции просто вызывают группу других, не выполняя никакой собственной работы. В нашем примере таких нет. Однако если убрать три оператора print из funct1, она станет именно такой координирующей функцией. Другие функции могут вызывать вспомогательные, а затем выполнять некую работу. В нашем примере funct1 — как раз такая: она вызывает другие функции, но при этом и сама совершает некие действия.

Есть группа функций, которые существуют сами по себе, не вызывая другие (за исключением, возможно, тех, которые уже поставляются с Python) ради помощи — мы будем называть их конечными, листовыми (leaf). Почему? Если представить все вызовы функций в виде большого дерева, то такие функции — листья, поскольку из них ничего не выходит. В нашем примере функции funct2, funct3 и funct4 являются конечными. В этой главе нас в первую очередь интересуют именно такие, но здесь и особенно в последующих главах вы увидите примеры и других видов функций.

ЧТО ЯВЛЯЕТСЯ АДЕКВАТНОЙ ЗАДАЧЕЙ ДЛЯ ФУНКЦИИ

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

Атрибуты хороших функций

Вот несколько рекомендаций, которые, по нашему мнению, помогут вам понять, что делает функцию хорошей.

  • Одна четкая задача для выполнения. Функции могут быть такими: «вычислить объем сферы», «найти наибольшее число в списке» или «проверить, содержит ли список определенное значение». Неконечные функции могут достигать более широких целей, например «обновить графику игры» или «собрать и очистить вводимые пользователем данные». Неконечные функции все равно должны иметь конкретную цель, но разрабатываются с учетом того, что для достижения своей цели, скорее всего, будут вызывать другие функции.
  • Четко определенное поведение. Задача «найти наибольшее число в списке» четко определена. Если я дам вам список чисел и попрошу найти самое большое число, то вы знаете, что делать. В отличие от этого задача «найти лучшее слово в списке» определена плохо. Вам нужно больше информации. Что такое «лучшее» слово? Это самое длинное, или то, в котором используется меньше всего гласных, или то, в котором нет ни одной буквы, которая есть в словах «Лео» или «Дэн»? Думаем, вы уловили суть; субъективные задачи не подходят для компьютеров. Вместо этого мы можем написать функцию «найти слово в списке, содержащее наибольшее количество символов», поскольку ожидаемая цель вполне определена. Часто программисты не могут указать все особенности функции только в ее названии, поэтому добавляют подробности в строку документации, чтобы прояснить использование. Если вы обнаружите, что описание поведения функции не укладывается в несколько предложений, то эта задача, вероятно, слишком велика для одной функции.
  • Малое количество строк кода. На протяжении многих лет мы слышали различные правила относительно длины функций, основанные на разных руководствах. Оптимальной будет длина от 12 до 20 строк кода Python, и это максимальное количество. В этих правилах количество строк используется в качестве косвенного показателя сложности кода, и это неплохое эмпирическое правило. Будучи сами программистами, мы оба применяем подобные правила к нашему коду, чтобы функции не становились слишком сложными. Использовать это правило как руководство к действию можно и работая с Copilot. Если вы даете ему указание найти функцию, а он возвращает 50 строк кода, то, скорее всего, это не очень удачные запрос или задача и, как мы обсуждали ранее, в таком количестве строк кода, скорее всего, будут ошибки.
  • Общее значение важнее конкретного использования. Функция, возвращающая количество значений в списке, превышающих 1, может играть важную роль в части вашей программы, но есть способ улучшить этот момент. Функцию следует переписать так, чтобы она возвращала количество значений в списке, которые больше другого параметра. Новая функция будет работать и для вашего случая (передайте функции 1 в качестве второго параметра), и для любого значения, отличающегося от 1. Мы стремимся к тому, чтобы функции были максимально простыми, но как можно более эффективными.
  • Четкий ввод и вывод. Обычно вам не нужно много параметров. Это не означает, что у вас не может быть много входных данных. Один параметр может представлять собой список элементов (о списках мы еще поговорим). Это означает только, что вы хотите найти способы свести количество вводных данных к минимуму. Вы можете вернуть только одно значение, но опять же можете вернуть и список, поэтому вы не настолько ограничены, как может показаться. Но если вы обнаружите, что пишете функцию, которая иногда возвращает список, иногда — одно значение, а иногда не возвращает ничего, то это, скорее всего, не очень хорошая функция.

Примеры хороших (и плохих) конечных функций

Вот примеры хороших функций:

  • вычислить объем сферы — учитывая радиус сферы, вернуть ее объем;
  • найти наибольшее число в списке — учитывая список, вернуть наибольшее значение;
  • проверить, содержит ли список определенное значение, — учитывая список и значение, вернуть True, если список содержит нужное значение, и False, если нет;
  • вывести состояние игры в шашки — учитывая двумерный список, представляющий игровую доску, вывести игровое поле на экран в виде текста;
  • вставить значение в список — учитывая список, новое значение и место в списке, вернуть новый список, который представляет собой старый список с новым значением, вставленным в нужное место.
Теперь приведем примеры плохих конечных функций и объясним, почему они плохие.
  • Запросить налоговую информацию пользователя и вернуть сумму, которую он должен заплатить в этом году. Возможно, в некоторых странах это было бы не так уж плохо, но мы не можем представить себе это как одну функцию ни в США, ни в Канаде, учитывая сложность налоговых правил!
  • Определить самое большое значение в списке и удалить его. Выглядит неплохо, однако на самом деле это две задачи. Первая — найти самое большое значение в списке. Вторая — удалить его из него. Мы рекомендуем использовать две функции: одну, которая находит наибольшее значение, и вторую, которая удаляет его из списка. Тем не менее если ваша программа должна выполнять эту задачу часто, то данная функция может стать хорошей неконечной функцией.
  • (Вспоминая наш набор данных из главы 2.) Вернуть имена квотербеков, на счету которых более 4000 ярдов на пасе в наборе данных. В таком запросе слишком много конкретики. Несомненно, число 4000 должно быть параметром, но, скорее всего, будет лучше сделать функцию, которая принимает в качестве входных данных позицию (квотербек, раннинбек), статистику (ярды на пасе, сыгранные матчи), и показатель, который нас интересует (4000, 8000) в качестве параметров. Эта новая функция предоставляет гораздо больше возможностей, чем изначальная, позволяя пользователю вызывать функцию для определения не только имен конкретных квотербеков, набравших более 4000 ярдов, но и имен раннинбеков, у которых было более 12 тачдаунов.
  • Определить лучший фильм всех времен и народов. Эта задача слишком расплывчата. Лучший фильм по какому критерию? Какие фильмы следует учитывать? Лучшей версией может быть функция, определяющая фильм с самым высоким рейтингом среди пользователей, учитывая хотя бы минимальное количество оценок. Эта функция, скорее всего, будет частью более крупной программы, в которой в качестве входных будут данные из базы данных фильмов (скажем, IMDB) и минимальное количество пользовательских оценок. Функция выведет фильм с наивысшим рейтингом, имеющий как минимум столько оценок, сколько указано.
  • Сыграть в Call of Duty. Это может быть функцией main в большой базе кода для игры Call of Duty, но это определенно не конечная функция.

ЦИКЛ ПРОЕКТИРОВАНИЯ ФУНКЦИЙ С ПОМОЩЬЮ COPILOT

Цикл проектирования функций с помощью Copilot содержит следующие этапы (рис. 3.3).

  1. Определите желаемое поведение функции.
  2. Напишите запрос, который описывает функцию максимально четко.
  3. Позвольте Copilot сгенерировать код.
  4. Прочитайте код, чтобы убедиться, что он выглядит подходящим.
  5. Протестируйте код, чтобы убедиться в его правильности:
    1. если после нескольких тестов код работает правильно, продолжайте работу;
    2. если код работает неправильно, перейдите к этапу 2 и отредактируйте запрос.
Мы не будем показывать, как выполнять этап 4, до следующей главы, но уверены, что вы уже можете распознать, когда код вопиюще неправильный. Например, Copilot может дать вам только комментарии для заполнения тела функции. Комментарии ничего не делают — это не код! Поэтому множество комментариев без какого-либо другого кода — явно не то, что нужно делать. Или Copilot может просто написать однострочный return -1. Или наш личный фаворит: Your code here. Этому Copilot научился у нас, профессоров, — мы даем студентам фрагмент кода и просим их написать остальное, использовав выражение «Ваш код здесь».

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

image
Рис. 3.3. Общий цикл редактирования с помощью Copilot. Предполагается, что вы определяете подходящую функцию

ПРИМЕРЫ СОЗДАНИЯ ХОРОШИХ ФУНКЦИЙ С ПОМОЩЬЮ COPILOT

В этом разделе мы напишем несколько функций с помощью Copilot, чтобы вы могли увидеть цикл их проектирования, о котором мы говорили чуть раньше. Наша цель в этой главе не состоит в том, чтобы помочь читать код; тем не менее мы покажем программные средства (иногда называемые конструкциями) в решениях, которые очень часто встречаются в коде (например, операторы if, циклы), поэтому будем специально указывать на них, когда они нам попадутся. Затем в главе 4 мы более подробно расскажем о том, как читать этот код.

Многие функции, над которыми мы будем работать, не связаны друг с другом. Например, мы начнем с функции, касающейся цен на акции, и перейдем к функции, касающейся надежных паролей. Обычно вы не храните такие несвязанные вещи в одном файле Python. Мы просто изучаем различные примеры хороших функций, поэтому не стесняйтесь хранить все функции в одном файле Python, назвав его, возможно, function_practice.py или ch3.py.

Выбор акций для Дэна

Дэн инвестирует в акции AAAPL. Он купил 10 акций по 15 долларов за штуку. Теперь каждая акция стоит $17. Дэн хотел бы узнать, сколько денег он заработал на этой сделке.

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

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

def money_made(num_shares, purchase_share_price, current_share_price):

Теперь нам нужны строки документации. В них мы должны объяснить, для чего нужен каждый параметр, используя его название в одном предложении. Вдобавок следует указать, что должна сделать функция.

Когда мы добавим строки документации, полный запрос для Copilot будет выглядеть так:

def money_made(num_shares, purchase_share_price, current_share_price):     """     num_shares — количество акций, которое мы приобрели.     purchase_share_price — цена каждой из этих акций.     current_share_price — текущая цена акции.      Выведи сумму денег, которую мы заработали на акции.     """

Набрав этот запрос, перейдите к следующей строке и нажмите клавишу Tab. Copilot заполнит код функции. Не беспокойтесь о том, что код будет с отступами: он и должен быть с ними, и если их нет, то это, по сути, ошибка!

Вот что мы получили от Copilot:

return num_shares * (current_share_price – purchase_share_price)

Этот код кажется подходящим. В скобках он вычисляет разницу между текущей ценой и ценой покупки (знак – используется для вычитания), а затем умножает эту разницу на количество акций, которые у нас есть (символ * используется для умножения). Проверка подобного кода — полезный навык, и мы серьезно займемся им в следующей главе. Еще один полезный навык — тестирование функции.

Чтобы протестировать функцию, мы вызываем ее, используя различные входные данные, и наблюдаем за выходом в каждом случае. Мы можем сделать это, попросив Copilot вызвать функцию, а затем запустив нашу программу, так же как ранее мы делали это с нашей функцией larger. Затем мы можем попросить Copilot изменить вызов функции, попросив его вызвать функцию с другими входными данными и снова запустить нашу программу, повторяя все это столько раз, сколько потребуется. Однако нам кажется более простым и удобным вызывать функцию самостоятельно из интерактивного окна.

Таким образом, мы можем вызывать функцию сколько угодно раз, не задействуя Copilot и не загромождая нашу программу тем, что мы все равно удалим. Чтобы попробовать этот интерактивный подход, выделите весь код функции и затем нажмите Shift+Enter (вы можете получить доступ к аналогичному интерактивному сеансу, выделив текст и щелкнув правой кнопкой мыши, затем выбрать Run Selection/Line (Выполнить выделение/строку) в окне Python, но в данном случае мы будем исходить из того, что вы используете Shift+Enter). На рис. 3.4 показано, как будет выглядеть экран, если выделить текст функции и нажать Shift+Enter.

image
Рис. 3.4. Запуск Python в интерактивном сеансе в терминале VS Code. Обратите внимание на символ >>> в нижней части терминала

В нижней части появившегося окна вы увидите три знака «больше»: >>>. Это называется подсказкой, и здесь вы можете вводить код Python. (Эта подсказка не имеет ничего общего с тем запросом, который мы используем при взаимодействии с Copilot.) Вы сразу же увидите результат работы набранного кода, что удобно и быстро.

Чтобы вызвать функцию money_made, нужно указать три аргумента, и они будут присвоены параметрам слева направо. То, что мы поставим первым, будет присвоено параметру num_shares, то, что вторым, — параметру purchase_share_price, а то, что третьим, — параметру current_share_price.

Ну что, попробуем! В подсказке введите >>> money_made(10, 15, 17) и нажмите Enter (или Shift+Enter). Не набирайте >>>, так как эти символы уже есть; мы добавляем их, чтобы было понятно, где мы набираем текст. На рис. 3.5 приведен пример запуска функции в терминале в подсказке Python:

>>> money_made(10, 15, 17)

image
Рис. 3.5. Вызов функции money_made из подсказки Python в терминале VS Code

Вы увидите результат:

20

Правильный ли он? Ну, мы купили 10 акций, и каждая из них подорожала на 2 доллара (с 15 до 17), так что мы заработали 20 долларов. Выглядит неплохо!

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

Как мы можем протестировать эту функцию по-другому? Мы ищем входные данные, которые каким-то образом относятся к другой категории. Одним из не очень хороших тестов сейчас было бы сказать: «Что, если наши акции вырастут с 15 до 18 долларов, а не с 15 до 17?» Это практически тот же тест, что и раньше, и есть шанс, что он сработает отлично.

Хорошая идея — проверить, что произойдет, когда пакет акций начнет падать в цене. В этом случае мы ожидаем получить отрицательный результат. И похоже, наша функция отлично работает с этой категорией тестов. Вот наш вызов функции и возвращенный результат:

>>> money_made(10, 17, 15) -20

Какие еще тесты мы можем провести? Ну, иногда цена на акции вообще не меняется. В этом случае мы ожидаем 0. Проверим:

>>> money_made(10, 15, 15) 0

Выглядит хорошо!

Тестирование — это сочетание науки и искусства. Сколько категорий можно протестировать? Действительно ли эти два вызова относятся к двум разным категориям? Не пропустили ли мы какие-нибудь категории? Вы улучшите свои способности к тестированию, практикуясь, и мы посвятим этому главу 6. На данный момент похоже, что наша функция money_made выполняет свою работу.

Функция может использовать переменные (а не только свои параметры) в коде, и мы хотим показать вам пример этого сейчас, чтобы вы были готовы увидеть, как это делает Copilot.

Эквивалентный способ написания кода для нашей функции money_made выглядит так:

price_difference = current_share_price - purchase_share_price return num_shares * price_difference

Возможно, вам будет даже проще прочитать этот код: сначала он вычисляет разницу в цене акций, а затем умножает ее на их количество. Мы рекомендуем протестировать эту версию, чтобы убедиться в ее правильности.

Пароль Лео

Лео регистрируется на сайте новой социальной сети ProgrammerBook. Он хочет убедиться, что его пароль надежен.

Лео начинает со скромного определения того, какой пароль является надежным: тот, в котором нет слов password и qwerty. (Это ужасные пароли, конечно, но в реальности мы должны дать гораздо более полное определение, чтобы гарантировать, что наш пароль надежен!) Полезной будет функция, которая принимает предложенный пароль и сообщает, является ли он надежным.

В отличие от предыдущих функций в этой главе, здесь мы не имеем дело с числами. Параметр, пароль для проверки, — это текст. А возвращаемое значение должно указывать некий результат «да/нет». Нам нужны новые типы!

Тип Python для текста называется строкой. Существуют миллионы возможных строк, поскольку мы можем использовать строку для хранения любого текста. А тип Python для «да/нет» называется логическим (Boolean). У него есть только два значения: True или False.

Отлично! Мы готовы дать Copilot запрос. Для функций, возвращающих логические значения (True/False), мы обычно называем функцию has_x, is_x или используем другой глагол, который подразумевает истинный/ложный результат:

def is_strong_password(password):     """     Надежный пароль — это не слово password     и не слово qwerty.      Верни True, если пароль надежен, и False, если нет.     """

В ответ на это Copilot написал:

return password != 'password' and password != 'qwerty'

Посмотрите на этот код. Оператор != — это оператор «не равно», а каждая строка, которая не считается надежным паролем, заключена в одинарные кавычки. Мы будем использовать их каждый раз, когда будем работать со строковым значением. Опять же о том, как читать код, мы поговорим в следующей главе, а пока надеемся, что код имеет ясный смысл: похоже, он проверяет, что паролем не являются слова password или qwerty, что соответствует нашему определению надежного пароля.

Теперь убедимся, что эта функция действительно работает, сделав несколько тестовых вызовов. Начнем с проверки паролей, которые не являются надежными. Мы должны получить ответ False для каждого из них — и мы это сделаем!

>>> is_strong_password('password') False  >>> is_strong_password('qwerty') False

Отлично! Попробуем ввести надежный пароль. Здесь мы должны увидеть True:

>>> is_strong_password('N3w Y0rk J375') True

Отлично! Теперь мы видим, что функция иногда возвращает False, а иногда True, так что по крайней мере не всегда возвращает False.

Попробуем еще раз:

>>> is_strong_password('Leo') True

Технически True — это правильно, но Leo — плохой пароль. Нам действительно следовало бы получше определить надежный пароль. Изменим наш запрос, чтобы дать более полное определение надежного пароля.

Вот наш следующий запрос и то, что выдал Copilot:

def is_strong_password(password):     """     Надежный пароль содержит минимум одну прописную букву,     минимум одну цифру и минимум один специальный символ.        Верни True, если пароль надежен, и False, если нет.     """     return any(char.isupper() for char in password) and ?              any(char.isdigit() for char in password) and ?              any(not char.isalnum() for char in password)

То, что выражение for char in password (символ в пароле) встречается несколько раз, показывает, как Python рассматривает каждый символ пароля. Данный способ позволяет использовать так называемый цикл (это ключевая тема, которую мы будем изучать в главе 5).

Конечно, вполне возможно, что вы получили от Copilot не тот же код, что и мы. Но, судя по коду, который получили мы, похоже, что первая часть ищет любой прописной символ, вторая — любую цифру, а третья — что-то, что не является isalnum.

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

>>> is_strong_password('Leo') False  >>> is_strong_password('N3w Y0rk J37$') True  >>> is_strong_password('N3w Y0rk J375') True

Ой! Мы получили неправильный ответ на третий пароль. Он выглядит довольно надежным, но это не так, поскольку в нем отсутствует специальный символ. Наша функция вернула True, но это неверное значение: она должна была вернуть False.

Код, который выдал Copilot, неправильный. Возможно, код, который вы получили, работает нормально, но нам не повезло, и важно знать, как определить (как мы сделали только что), когда код неверен, и затем исправить его. Сейчас не так важно знать, почему он неправильный, но если вам интересно, почему наш запрос мог привести к неправильному поведению, то это потому, что мы не указали, что подразумеваем под специальным символом, ведь в коде, который мы получили, пробелы считаются специальными символами.

На данный момент у нас есть два варианта работы с неправильным кодом. Первый вариант — нажать Ctrl+Enter и просмотреть другие предложения, которые выдает Copilot. Возможно, одно из них будет связано со специальными символами, которые выглядят иначе, чем несработавшие?

Действительно, на нашем компьютере мы видим такие варианты, в том числе и этот:

image
Протестируйте эту функцию со строками типа Leo, N3w Y0rk J375 и N3w Y0rk J375$ — и увидите, что функция работает хорошо.

Второй вариант — немного поработать над запросом, то есть настроить его так, чтобы повлиять на выдачу Copilot. В первоначальном запросе мы говорили о специальных символах. Оглядываясь назад, можно сказать, что наша речь была расплывчатой. Возможно, мы имели в виду что-то более конкретное, например пунктуацию. Если мы в запросе говорим конкретно о пунктуации, то взаимодействие с Copilot будет выглядеть так:

def is_strong_password(password):     """     Надежный пароль содержит минимум одну прописную букву,     минимум одну цифру и минимум один специальный символ.      Верни True, если пароль надежен, и False, если нет.     """     return any(char.isupper() for char in password) and ?            any(char.isdigit() for char in password) and ?            any(char in string.punctuation for char in password)

Выглядит хорошо! В последней строке говорится о пунктуации, что внушает надежду. Проверим:

>>> is_strong_password('Leo') False  >>> is_strong_password('N3w Y0rk J375')  Traceback (most recent call last):   File "<stdin>", line 1, in <module>   File "ch2.py", line 44, in is_strong_password     any(char in string.punctuation for char in password)   File "ch2.py", line 44, in <genexpr>     any(char in string.punctuation for char in password)                 ^^^^^^ NameError: name 'string' is not defined

Посмотрите на нижнюю часть сообщения об ошибке, 'string' не определена, так ведь? Мы сталкиваемся с проблемой, похожей на ту, которую видели в главе 2, когда говорили о модулях. Copilot хочет использовать модуль под названием string, но этот модуль должен быть импортирован, прежде чем мы сможем его задействовать. В Python существует множество модулей, но string хорошо известен. По мере того как вы будете работать с Copilot, вы узнаете, какие модули используются часто, чтобы понимать, как их импортировать. Вы также можете провести быстрый поиск в Интернете на тему «Является ли string модулем Python», и результаты подтвердят, что это так. Что нам нужно сделать, так это импортировать модуль.

Обратите внимание, что эта ситуация несколько отличается от той, с которой мы сталкивались в главе 2. Там мы видели, что происходит, когда код из Copilot импортирует модули, которые у нас не установлены, и, чтобы решить проблему, нам пришлось установить пакет с ними. В данном случае код из Copilot использует модуль, который уже установлен вместе с Python, но он забыл его импортировать. Таким образом, нам не нужно устанавливать string; нам надо просто импортировать его.

ИМПОРТИРОВАНИЕ МОДУЛЕЙ

В Python есть множество полезных модулей. В главе 2 мы видели, насколько эффективным модулем является Matplotlib. Но, чтобы код Python мог использовать преимущества модуля, мы должны импортировать сам модуль. Вы можете спросить, почему бы нам не иметь доступ к модулям, не импортируя их. Но это значительно усложнило бы код и все те действия, которые Python приходится выполнять, чтобы запустить код «за кулисами». Вместо этого модель заключается в том, чтобы добавлять модули, если вы хотите их использовать, а по умолчанию они не добавлены.

Добавим import string в начало нашего кода:

import string  def is_strong_password(password):     """     Надежный пароль содержит минимум одну прописную букву,     минимум одну цифру и минимум один специальный символ.      Верни True, если пароль надежен, и False, если нет.     """     return any(char.isupper() for char in password) and ?            any(char.isdigit() for char in password) and ?            any(char in string.punctuation for char in password)

Теперь код работает хорошо:

>>> is_strong_password('Leo') False  >>> is_strong_password('N3w Y0rk J375') False  >>> is_strong_password('N3w Y0rk J375$') True

Последнее слово — True — говорит о том, что это надежный пароль, поскольку к нему добавлен знак препинания $.

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

Более подробно с книгой можно ознакомиться на сайте издательства: » Оглавление » Отрывок По факту оплаты бумажной версии книги на e-mail высылается электронная книга. Для Хаброжителей скидка 25% по купону — Python

Источник: habr.com

Комментарии: