Функция которая ничего не делает python
Перейти к содержимому

Функция которая ничего не делает python

  • автор:

4. Дополнительные средства управления потоком¶

Кроме ранее рассмотренного оператора while , в Python доступны привычные операторы управления потоком из других языков, но с некоторыми особенностями.

4.1. Оператор if ¶

Возможно, наиболее широко известный тип оператора — оператор if . Например:

Может быть ноль или более elif блоков, а else блок опционален. Ключевое слово „ elif “ является короткой записью „else if“ и полезно во избежание чрезмерного углубления. Оператор if … elif … elif … является заменой оператора switch или case , которые можно встретить в других языках программирования.

4.2. Оператор for ¶

Оператор for в Python немного отличается от используемого в C или Pascal. Вместо неизменного прохождения по арифметической прогрессии из чисел (как в Pascal) или предоставления пользователю возможности указать шаг итерации и условие остановки (как в С), оператор for в Python проходит по всем элементам любой последовательности (списка или строки) в том порядке, в котором они в ней располагаются. Например (игра слов не подразумевалась):

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

4.3. Функция range() ¶

Если необходима итерация по последовательности чисел, пригодится встроенная функция range() . Она генерирует арифметические прогрессии:

Конечная точка никогда не является частью сгенерированной последовательности; range(10) производит 10 значений, которые являются допустимыми индексами для элементов последовательности длиной 10. Также range может начинаться с другого числа или иметь другое приращение (даже отрицательное; иногда это называется «шагом»):

Для итерации индексов последовательности можно объединить range() и len() следующим образом:

Однако в большинстве таких случаев удобнее использовать функцию enumerate() , см. в Методах перебора элементов .

Если просто напечатать range, то происходит что-то странное:

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

Мы называем такие объекты итерируемыми , и это все объекты, которые предназначаются для функций и конструкций, ожидающих от них поочерёдного предоставления элементов до тех пор, пока источник не иссякнет. Мы видели, что оператор for является такой конструкцией, в то время как пример функции который принимает итерируемое значение sum() :

Позже мы увидим больше функций, которые возвращают и принимают итерируемые объекты как аргументы. Наконец, может быть, вам интересно, как получить список из range. Решение:

В главе Структуры данных мы рассмотрим более подробно list() .

4.4. Операторы break и continue , а также уточнение циклов else ¶

Как и в случае с C, оператор break выходит из самой внутренней обёртки цикла for или while .

Операторы цикла могут содержать уточнение else ; выполняется при выполнении цикла заканчивается исчерпанием итератива (с for ) или когда состояние становится ложным (с while ), но это не тот случай, если цикл прерван оператором break . Это поведение иллюстрируется следующим примером, в котором производится поиск простых чисел:

Да, это правильный код. Внимательно посмотрите на замечание else принадлежащее циклу for , а не if оператору.

При использовании с циклом уточнение else имеет больше общего с else уточнения оператора try , чем уточнение if оператора. Уточнение else оператора try выполняется при отсутствии исключений и выполнение уточнения else цикла при отсутствии break . Дополнительные сведения об операторе try и исключениях см. в разделе Обработка исключений .

Оператор continue также заимствован у C, переходя к следующей итерации цикла:

4.5. Оператор pass ¶

Оператор pass ничего не делает. Он может использоваться когда синтаксически требуется присутствие оператора, но от программы не требуется действий. Например:

Он обычно используется для создания минималистичных классов:

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

4.6. Определение функций¶

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

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

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

Исполнение функции приводит к созданию новой таблицы символов, использующейся для хранения локальных переменных функции. Если быть более точными, все присваивания переменных в функции сохраняют значение в локальной таблице символов; при обнаружении ссылки на переменную, в первую очередь просматривается локальная таблица символов, затем локальная таблица символов для окружающих функций, затем глобальная таблица символов и, наконец, таблица встроенных имён. Таким образом, глобальным переменным и переменным вложенных функций не может быть непосредственно присвоено значение в пределах функции (для глобальных переменных, именованных оператором global , или, для переменных внешних функций, именованных оператором nonlocal ), несмотря на то, что ссылки на них могут использоваться.

Фактические параметры (аргументы) при вызове функции помещаются в локальной таблице символов вызываемой функции, когда она вызывается; таким образом аргументы передаются по значению (где значение всегда является ссылкой на объект, а не значением объекта). [1] Если одна функция вызывает другую — то для этого вызова создаётся новая локальная таблица символов.

Определение функции связывает имя функции с объектом функции в текущей таблице символов. Интерпретатор распознает объект, на который указывает это имя, как определяемую пользователем функцию. Другие имена также могут указывать на тот же объект функции и также могут использоваться для доступа к этой функции:

Сравнивая с другими языками, можно возразить, что fib не является функцией, а процедурой, так как она не возвращает значение. Фактически даже работает без оператора return , при этом всё-таки возвращает значение, пусть и довольно посредственное. Это значение называется None (встроенное имя). Вывод значения None обычно подавляется в интерактивном режиме интерпретатора, если оно оказывается единственным значением, которое нужно вывести. Вы можете проследить за этим процессом, если действительно хотите, используя функцию print() :

Далее представлена функция, возвращающая список чисел последовательности Фибоначчи, вместо её печати в командную строку:

Этот пример, как обычно, демонстрирует некоторые новые возможности Python:

  • Оператор return возвращает значение из функции. return без аргумента возвращает None . Пропуск оператора в конце функции также возвращает None .
  • Оператор result.append(a) вызывает метод объекта списка result . Метод — это функция, которая «принадлежит» объекту и указывается через выражение вида obj.methodname , где obj — некоторый объект (это может быть выражение), а methodname — имя метода, определяемое типом объекта. Различные типы определяют различные методы. Методы различных типов могут иметь одно и то же имя, не вызывая неоднозначности. (Можно определить собственные типы объектов и методов, используя классы, см. Классы ). Метод append() , показанный в примере, определён для объектов списка, добавляя новый элемент в конец списка. В этом примере он эквивалентен result = result + [a] , но более эффективно.

4.7. Дополнительные сведения об определении функций¶

Также есть возможность определять функции с переменным количеством параметров. Для этого существует три формы, которые также можно использовать совместно.

4.7.1. Значения аргументов по умолчанию¶

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

Эту функцию можно вызвать несколькими способами:

  • Передав только обязательный аргумент: ask_ok(‘Вы действительно хотите выйти?’)
  • Передав только один опциональный аргумент ask_ok(‘Можно перезаписать файл?’, 2)
  • Или передав все аргументы: ask_ok(‘Вы точно хотите перезаписать файл?’, 2, ‘Хватит, либо yes, либо no!’)

В этом примере также вводится ключевое слово in . При этом проверяется, содержит или нет последовательность определенное значение.

Значения по умолчанию вычисляются в точке определения функции в определяющей области видимости, поэтому код:

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

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

4.7.2. Ключевые аргументы¶

Функции также можно вызывать с помощью ключевых аргументов формы kwarg=value . Например:

Принимает один обязательный аргумент ( voltage ) и три необязательных аргумента ( state , action и type ). Функция может быть вызвана в одним из следующих способов:

Но все следующие вызовы будут недопустимы:

При вызове функции ключевые аргументы должны следовать за позиционными аргументами. Все переданные ключевые аргументы должны соответствовать одному из аргументов принимаемых функцией (например actor не является допустимым аргументом для функции parrot ) и их порядок не важен. Это также справедливо и для необязательных аргументов (например, parrot(voltage=1000) также допустим). Ни один аргумент не может получить значение более одного раза. Далее пример, который не удается выполнить из-за этого ограничения:

Если в определении функции присутствует завершающий параметр в виде **name он получает словарь (см. Типы сопоставления — dict ), содержащий все ключевые аргументы, кроме тех, которые соответствуют формальному параметру. Можно совместить эту особенность с поддержкой формального параметра в формате *name (описан в следующем подразделе), получает кортеж , содержащий позиционные аргументы за пределами списка формальных параметров. ( *name должен быть перед **name .) Например, если определить такую функцию:

то её можно будет вызвать так:

И, конечно, она напечатала бы:

Обратите внимание, что порядок печати ключевых аргументов гарантирован. Для соответствия порядку, в котором они были предоставлены в вызове функции.

4.7.3. Специальные параметры¶

По умолчанию аргументы могут передаваться Python функции либо по позиции или явно по ключевому аргументу. Для удобочитаемости и производительности имеет смысл ограничить способ передачи аргументов, чтобы разработчик сразу мог понять по определению функции передаются ли элементы по позициям, по позиции или ключевому аргументу, или по ключевому аргументу.

Определение функции может выглядеть следующим образом:

Где / и * являются необязательными. При использовании этих символов определяют способ передачи аргументов в функцию: позиционно, позиционно-ключевым, и только ключевым. Ключевые аргументы также называют именованными параметрами.

4.7.3.1. Позиционные или ключевые аргументы¶

Если / и * отсутствуют в определении функции, аргументы могут передаться функции по позиции или по ключевому аргументу.

4.7.3.2. Только позиционные параметры¶

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

Параметры, следующие за / , могут быть позиционными или ключевыми или только ключевыми.

4.7.3.3. Только ключевые аргументы¶

Чтобы пометить параметры как только ключевые, необходимо передать их по ключевому аргументу поместив * в список аргументов непосредственно перед первым только ключевым параметром.

4.7.3.4. Примеры функции¶

Рассмотрим следующие примеры определений функций с акцентированием внимания на маркеры / и * :

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

Вторая функция pos_only_arg ограничена только позиционными аргументами при наличии / в определении функции:

* в определении функции:

И последняя использует все три соглашения в одном определении функции:

Рассмотрим определение функции, у которой есть потенциальная коллизия между позиционным аргументом name и **kwds , а также есть name в качестве ключа:

Нет никакого возможного вызова, который заставит его вернуть True , т. к. ключевой аргумент ‘name’ всегда будет привязан к первому параметру. Например:

Но использование / (только позиционные аргументы) делает возможным использовать name как позиционный аргумент и ‘name’ как ключ в ключевых аргументах:

Другими словами, имена только позиционных параметров могут использоваться в **kwds без двусмысленности.

4.7.3.5. Резюме¶

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

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

4.7.4. Произвольные списки аргументов¶

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

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

4.7.5. Распаковка списка аргументов¶

Происходит обратная ситуация когда параметры уже содержатся в списке или в кортеже, но должны быть распакованы для вызова функции, требующей отдельных позиционных параметров. Например, встроенная функция range() ожидает отдельные старт и стоп аргументы. Если они не доступны раздельно, для распаковки аргументов из списка или кортежа в вызове функции используйте * -синтаксис:

Схожим способом, словари могут получать ключевые аргументы через оператор ** :

4.7.6. Лямбда-выражения¶

Небольшие анонимные функции могут быть созданы с помощью ключевого слова lambda . Функция возвращает сумму своих двух аргументов: lambda a, b: a+b . Лямбда-функции можно использовать везде, где требуются объекты функции. Они синтаксически ограничены одним выражением. Семантически, они лишь «синтаксический сахар» для обычного определения функции. Как и определения вложенных функций, лямбда-функции могут ссылаться на переменные из содержащей их области видимости:

Пример выше использует лямбда-выражение для возврата функции. Другое применение — передать маленькую функцию как аргумент::

4.7.7. Докстринги¶

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

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

Парсер Python не обрабатывает отступы в многострочных литералах, поэтому инструментам, которые работают над документацией, предлагается, по желанию, делать это самим. Производится это по следующему соглашению. Первая непустая строка после первой строки литерала определяет величину отступа всего литерала документации. (Мы не можем использовать первую строку, поскольку она обычно выравнивается по открывающим кавычкам и её отступ в литерале не явен). Пробельный «эквивалент» этого отступа затем отрезается от начала всех строк литерала. Строк с меньшим отступом не должно обнаруживаться, но если они встретились, весь их начальный отступ должен быть обрезан. Эквивалентность пробельных замен может быть протестирована развертыванием табуляции (обычно, к 8 пробелам).

Далее пример многострочного докстринга:

4.7.8. Аннотации функций¶

Аннотация функций совершенно необязательная мета-информация о типах, используемых функциями, определяемых пользователем (см. PEP 3107 и PEP 484 для получения дополнительной информации).

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

4.8. Интермеццо: стиль написания кода¶

Теперь, когда вам предстоит писать более объёмные, более сложные блоки кода на Python, пришло время поговорить о стиле кода. В большинстве языков код может быть написан (или более кратко, отформатирован) различными стилями; некоторые из них более удобочитаемы, чем другие. Стремление к написанию лёгкого для прочтения другими кода всегда считалось хорошим тоном, и выбор правильного стиля для кода крайне ему способствует.

В случае языка Python PEP 8 стал руководством по стилю, которого придерживается большинство проектов. Он способствует написанию легко читаемому и приятному для глаз стилю кодирования. Каждый Python разработчик обязан ознакомиться с ним. Далее представлены кратное содержание наиболее важных пунктов:

Используйте отступ из 4 пробелов, без табуляций.

4 пробела легко опознаются и в случае небольших отступов (хватает места для глубоких вложений) и в случае больших отступов (приятнее читается). Табуляция вносит путаницу и лучше от неё воздержаться.

Разделяйте строки так, чтобы их длина не превышала 79-и символов

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

Используйте пустые строки для отделения функций, классов, и крупных блоков внутри функций.

По возможности располагайте комментарий на отдельной строке.

Используйте пробелы вокруг операторов и после запятых, но не добавляйте их внутри конструкций в скобках: a = f(1, 2) + g(3, 4) .

Называйте ваши классы и функции единообразно; соглашение следующее: используйте UpperCamelCase для классов и lowercase_with_underscores для функций и методов. Всегда используйте self в качестве имени для первого аргумента метода (см. раздел Первый взгляд на классы для получения дополнительных сведений о методах и классах).

Не используйте в вашем коде изощрённых кодировок, если он рассчитан на использование в интернациональной среде. Умолчание Python, UTF-8, или даже простой набор ASCII всегда работает на ура.

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

break, continue, pass#

В Python есть несколько операторов, которые позволяют менять поведение циклов по умолчанию.

Оператор break#

Оператор break позволяет досрочно прервать цикл:

break прерывает текущий цикл и продолжает выполнение следующих выражений

если используется несколько вложенных циклов, break прерывает внутренний цикл и продолжает выполнять выражения, следующие за блоком * break может использоваться в циклах for и while

Пример с циклом for:

Пример с циклом while:

Использование break в примере с запросом пароля (файл check_password_with_while_break.py):

Теперь можно не повторять строку password = input(‘Введите пароль еще раз: ‘) в каждом ответвлении, достаточно перенести ее в конец цикла.

И, как только будет введен правильный пароль, break выведет программу из цикла while.

Оператор continue#

Оператор continue возвращает управление в начало цикла. То есть, continue позволяет «перепрыгнуть» оставшиеся выражения в цикле и перейти к следующей итерации.

Пример с циклом for:

Пример с циклом while:

Использование continue в примере с запросом пароля (файл check_password_with_while_continue.py):

Тут выход из цикла выполнятся с помощью проверки флага password_correct. Когда был введен правильный пароль, флаг выставляется равным True, и с помощью continue выполняется переход в начало цикла, перескочив последнюю строку с запросом пароля.

Результат выполнения будет таким:

Оператор pass#

Оператор pass ничего не делает. Фактически, это такая заглушка для объектов.

Например, pass может помочь в ситуации, когда нужно прописать структуру скрипта. Его можно ставить в циклах, функциях, классах. И это не будет влиять на исполнение кода.

Определение функции, которая ничего не делает

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

do_stuff это функция, определенная где-то еще, которая выполняет полезные вычисления. я хочу bar() быть определенным, чтобы ничего не делать, если указанное выше условие не выполняется. Есть идеи?

Примечание: я разработал блок if-else выше для иллюстрации. Я знаю, что я мог бы просто позвонить do_stuff() в if тело и pass в else тело. Код выше просто для иллюстрации блока кода, после которого мне нужно bar быть определенным или сделать что-то или ничего не делать. Блок просто определяет функцию и не предназначен для ее вызова.

2 ответа

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

Используя определение функции:

Вы также можете использовать return None вместо pass для определения функции поведение эквивалентно.

Исключения в Python теперь считаются анти-паттерном

Что такое исключения? Из названия понятно — они возникают, когда в программе происходит исключительная ситуация. Вы спросите, почему исключения — анти-паттерн, и как они вообще относятся к типизации? Я попробовал разобраться, и теперь хочу обсудить это с вами, хабражители.

Проблемы исключений

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

Исключения трудно заметить

Существует два типа исключений: «явные» создаются при помощи вызова raise прямо в коде, который вы читаете; «скрытые» запрятаны в используемых функциях, классах, методах.

Проблема в том, что «скрытые» исключения и правда трудно заметить. Покажу на примере чистой функции:

Функция просто делит одно число на другое, возвращая float . Типы проверены и можно запустить что-то такое:

Заметили? На самом деле до print исполнение программы никогда не дойдет, потому что деление 1 на 0 – невозможная операция, она вызовет ZeroDivisionError . Да, такой код безопасен с точки зрения типов, но его все равно нельзя использовать.

Чтобы заметить потенциальную проблему даже в таком максимально простом и читаемом коде, нужен опыт. Все что угодно в Python может перестать работать с разными типами исключений: деление, вызовы функций, int , str , генераторы, итераторы в циклах, доступ к атрибутам или ключам. Даже сам raise something() может привести к сбою. Причем, я даже не упоминаю операции ввода и вывода. А проверенные исключения перестанут поддерживаться в ближайшем будущем.

Восстановление нормального поведения на месте невозможно

Но именно на такой случай у нас же есть исключения. Давайте просто обработаем ZeroDivisionError , и код станет безопасным с точки зрения типов.

Теперь всё в порядке. Но почему мы возвращаем 0? Почему не 1 или None ? Конечно, в большинстве случаев, получить None почти так же плохо (если даже не хуже), как исключение, но все же нужно опираться на бизнес-логику и варианты использования функции.

Что именно мы делим? Произвольные числа, какие-то конкретные единицы или деньги? Не каждый вариант легко предусмотреть и восстановить. Может получиться, что при последующем использовании одной функции обнаружится, что потребуется другая логика восстановления.

Нет серебряной пули, которая бы справилась с ZeroDivisionError раз и навсегда. И это мы ещё не говорим о возможности сложного ввода-вывода с повторными запросами и таймаутами.

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

Процесс выполнения неясен

Хорошо, давайте понадеемся, что кто-то другой поймает исключение и, возможно, справится с ним. Например, система может запросить у пользователя изменить введенное значение, потому что нельзя делить на 0. И функция divide явно не должна отвечать за восстановление после ошибки.

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

Невозможно определить, какая строка кода выполнится после создания исключения. Разные типы исключений могут обрабатываться с помощью разных вариантов except , а некоторые исключения могут игнорироваться. А еще вы можете выкинуть дополнительные исключения в других модулях, которые выполнятся раньше, и вообще сломают всю логику.

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

Только с включенным отладчиком в режиме «ловить все исключения».

Исключения, как пресловутое goto , рвут структуру программы.

Исключения не исключительны

Посмотрим на другой пример: обычный код доступа к удаленному HTTP API:

В этом примере буквально все может пойти не так. Вот неполный список возможных ошибок:

  • Сеть может быть недоступна, и запрос вообще не будет выполняться.
  • Может не работать сервер.
  • Сервер может быть слишком занят, наступит таймаут.
  • Сервер может потребовать аутентификацию.
  • У API может не быть такого URL.
  • Может быть передан несуществующий пользователь.
  • Может быть недостаточно прав.
  • Сервер может упасть из-за внутренней ошибки при обработке вашего запроса
  • Сервер может вернуть невалидный или поврежденный ответ.
  • Сервер может вернуть невалидный JSON, который не удастся распарсить.

Как себя обезопасить?

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

  • Везде написать except Exception: pass . Тупиковый путь. Не делайте так.
  • Возвращать None . Тоже зло. В итоге либо придется почти каждую строку начинать с if something is not None: и вся логика потеряется за мусором очищающих проверок, либо все время страдать от TypeError . Не самый приятный выбор.
  • Писать классы для особых случаев использования. Например, базовый класс User с подклассами для ошибок типа UserNotFound и MissingUser . Такой подход вполне можно использовать в некоторых конкретных ситуациях, таких как AnonymousUser в Django, но обернуть все возможные ошибки в классы нереально. Потребуется слишком много работы, и доменная модель станет невообразимо сложной.
  • Использовать контейнеры, чтобы обернуть полученное значение переменной или ошибки в обертку и дальше работать уже со значением контейнера. Вот почему мы создали проект @dry-python/return . Чтобы функции возвращали что-то осмысленное, типизированное и безопасное.

Заключим значения в одну из двух оберток: Success или Failure . Данные классы наследуются от базового класса Result . Типы упакованных значений можно указать в аннотации возвращаемой функцией, например, Result[float, ZeroDivisionError] возвращает либо Success[float] , либо Failure[ZeroDivisionError] .

Что это нам дает? Больше исключения не исключительные, а представляют собой ожидаемые проблемы. Также оборачивание исключения в Failure решает вторую проблему: сложность определения потенциальных исключений.

Теперь их легко заметить. Если видите в коде Result , значит функция может выдать исключение. И вы даже заранее знаете его тип.

Более того, библиотека полностью типизирована и совместима с PEP561. То есть mypy предупредит вас, если вы попытаетесь вернуть что-то, что не соответствует объявленному типу.

Как работать с контейнерами?

  • map для функций, которые возвращают обычные значения;
  • bind для функций, которые возвращают другие контейнеры.

Прелесть в том, что такой код защитит вас от неудачных сценариев, поскольку .bind и .map не выполнятся для контейнеров c Failure :

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

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

Но как развернуть значения из контейнеров?

Действительно, если вы работаете с функциями, которые ничего не знают про контейнеры, вам нужны именно сами значения. Тогда можно использовать методы .unwrap() или .value_or() :

Подождите, мы должны были избавиться от исключений, а теперь выясняется, что все вызовы .unwrap() могут привести к еще одному исключению?

Как не думать об UnwrapFailedErrors?

Хорошо, давайте посмотрим, как жить с новыми исключениями. Рассмотрим такой пример: нужно проверить пользовательский ввод и создать две модели в базе данных. Каждый шаг может завершиться исключением, вот почему все методы обернуты в Result :

Во-первых, можно вообще не разворачивать значения в собственной бизнес-логике:

Все сработает без каких-либо проблем, не вызовутся никакие исключения, потому что не используется .unwrap() . Но легко ли читать такой код? Нет. А какая есть альтернатива? @pipeline :

Теперь данный код отлично читается. Вот как .unwrap() и @pipeline работают вместе: всякий раз, когда какой-либо метод .unwrap() завершается неудачей и Failure[str] , декоратор @pipeline ловит её и возвращает Failure[str] в качестве результирующего значения. Вот так я предлагаю удалить все исключения из кода и сделать его действительно безопасным и типизированным.

Оборачиваем все вместе

Хорошо, теперь применим новые инструменты к примеру с запросом к HTTP API. Помните, что каждая строка может вызвать исключение? И нет никакого способа заставить их вернуть контейнер с Result . Но можно использовать декоратор @safe, чтобы обернуть небезопасные функции и сделать их безопасными. Ниже два варианта кода, которые делают одно и то же:

Первый, с @safe , проще и лучше читается.

Последнее, что нужно сделать в примере с запросом к API – добавить декоратор @safe . В итоге получится такой код:

Подведем итог, как избавиться от исключений и обезопасить код:

  • Использовать обертку @safe для всех методов, которые могут вызвать исключение. Она изменит тип возвращаемого значения функции на Result[OldReturnType, Exception] .
  • Использовать Result как контейнер, чтобы перенести значения и ошибки в простую абстракцию.
  • Использовать .unwrap() , чтобы развернуть значение из контейнера.
  • Использовать @pipeline , чтобы последовательности вызовов .unwrap легче читались.
  • «Исключения трудно заметить». Теперь они обернуты в типизированный контейнер Result , что делает их совершенно прозрачными.
  • «Восстановление нормального поведения на месте невозможно». Теперь можно смело делегировать процесс восстановления вызывающей стороне. На такой случай есть .fix() и .rescue() .
  • «Последовательность исполнения неясна». Теперь они едины с обычным бизнес-потоком. От начала и до конца.
  • «Исключения не являются исключительными». Мы знаем! И мы ожидаем, что что-то пойдет не так и готовы ко всему.

Варианты использования и ограничения

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *