Грамотное использование assert в Python или прикрой свой з**!

Андрей Волков

Андрей Волков

Системное, сетевое администрирование +DBA. И немного программист!))  Профиль автора.

использование assert  в PythonИногда по-настоящему полезное функциональное средство языка при­влекает меньше внимания, чем оно того заслуживает. По некоторым причинам это именно то, что произошло со встроенной в Python инструк­цией assert.

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



 

В этом месте вы, вероятно, заинтересуетесь: «Что такое assert и в чем ее прелесть?» Позвольте дать вам несколько ответов.

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

 

Инструкция assert в Python — пример

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

Предположим, вы создаете интернет-магазин с помощью Python. Вы рабо­таете над добавлением в систему функциональности скидочного купона, и в итоге вы пишете следующую функцию apply_discount:

def apply_discount(product, discount):
   price = int(product['цена'] * (1.0 — discount))
   assert 0 <= price <= product['цена']
   return price

Вы заметили, что здесь есть инструкция assert? Она будет гарантировать, что, независимо от обстоятельств, вычисляемые этой функцией снижен­ные цены не могут быть ниже 0 $ и они не могут быть выше первоначаль­ной цены товара.

Давайте убедимся, что эта функция действительно работает как заду­мано, если вызвать ее, применив допустимую скидку. В этом примере товары в нашем магазине будут представлены в виде простых слова­рей. И скорее всего, в реальном приложении вы примените другую структуру данных, но эта безупречна для демонстрации утверждений assert. Давайте создадим пример товара — пару симпатичных туфель по цене 149,00 $:

>>> shoes = {'имя': 'Модные туфли', 'цена': 14900}

Кстати, заметили, как я избежал проблем с округлением денежной цены, использовав целое число для представления цены в центах? В целом не­плохое решение... Но я отвлекся. Итак, если к этим туфлям мы применим 25 %-ную скидку, то ожидаемо придем к отпускной цене 111,75 $:

>>> apply_discount(shoes, 0.25)
11175

Отлично, функция сработала безупречно. Теперь давайте попробуем при­менить несколько недопустимых скидок. Например, 200%-ную «скидку», которая вынудит нас отдать деньги покупателю:

>>> apply_discount(shoes, 2.0)
Traceback (most recent call last):
   File "<input>", line 1, in <module>
      apply_discount(prod, 2.0)
   File "<input>", line 4, in apply_discount
      assert 0 <= price <= product['price']
AssertionError

Как вы видите, когда мы пытаемся применить эту недопустимую скидку, наша программа останавливается с исключением AssertionError. Это происходит потому, что 200 %-ная скидка нарушила условие утверждения assert, которое мы поместили в функцию apply_discount.

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

Это значительно ускорит отладку и в дальнейшем сделает ваши про­граммы удобнее в поддержке. А в этом, дружище, как раз и заключается сила assert!

 

Почему просто не применить обычное исключение?

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

Дело в том, что инструкция assert предназначена для того, чтобы сооб­щать разработчикам о неустранимых ошибках в программе. Инструкция assert не предназначена для того, чтобы сигнализировать об ожидаемых ошибочных условиях, таких как ошибка «Файл не найден», где пользователь может предпринять корректирующие действия или просто попро­бовать еще раз.

Инструкции призваны быть внутренними самопроверками (internal self­checks) вашей программы. Они работают путем объявления неких усло­вий, возникновение которых в вашем исходном коде невозможно. Если одно из таких условий не сохраняется, то это означает, что в программе есть ошибка.

Если ваша программа бездефектна, то эти условия никогда не возникнут. Но если же они возникают, то программа завершится аварийно с исклю­чением AssertionError, говорящим, какое именно «невозможное» усло­вие было вызвано. Это намного упрощает отслеживание и исправление ошибок в ваших программах. А мне нравится все, что делает жизнь легче. Надеюсь, вам тоже.

А пока имейте в виду, что инструкция assert — это средство отладки, а не механизм обработки ошибок времени исполнения программы. Цель использования инструкции assert состоит в том, чтобы позволить разра­ботчикам как можно скорее найти вероятную первопричину ошибки. Если в вашей программе ошибки нет, то исключение AssertionError никогда не должно возникнуть.

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

 

Прежде чем вы начнете применять какое-то функциональное средство языка, всегда неплохо подробнее познакомиться с тем, как оно практиче­ски реализуется в Python. Поэтому давайте бегло взглянем на синтаксис инструкции assert в соответствии с документацией Python:

инструкция_assert ::= "assert" выражение1 ["," выражение2]

 

В данном случае выражение1 — это условие, которое мы проверяем, а не­обязательное выражение2 — это сообщение об ошибке, которое выводится на экран, если утверждение дает сбой. Во время исполнения программы интерпретатор Python преобразовывает каждую инструкцию assert при­мерно в следующую ниже последовательность инструкций:

if __debug__:
   if not выражение1:
      raise AssertionError(выражение2)

В этом фрагменте кода есть две интересные детали.

Перед тем как данное условие инструкции assert будет проверено, про­водится дополнительная проверка глобальной переменной __debug__. Это встроенный булев флажок, который при нормальных обстоятельствах имеет значение True, — и значение False, если запрашивается оптимиза­ция. Мы поговорим об этом подробнее чуть позже в разделе, посвященном «распространенным ловушкам».

Кроме того, вы можете применить выражение2, чтобы передать необязатель­ное сообщение об ошибке, которое будет показано в отчете об обратной трассировке вместе с исключением AssertionError. Это может еще больше упростить отладку. Например, я встречал исходный код такого плана:

>>> if cond == 'x':
...    do_x()
... elif cond == 'y':
...    do_y()
... else:
...    assert False, (
...       'Это никогда не должно произойти, и тем не менее это '
...       'временами происходит. Сейчас мы пытаемся выяснить'
...       'причину. Если вы столкнетесь с этим на практике, то '
...       'просим связаться по электронной почте с dbader. Спасибо!')

Разве это не ужасно? Конечно, да. Но этот прием определенно допустим и полезен, если в одном из своих приложений вы сталкиваетесь с плава­ющей ошибкой Гейзенбаг.

 

Распространенные ловушки, связанные с использованием инструкции assert в Python

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

Первое из них связано с внесением в приложения ошибок и рисков, свя­занных с нарушением безопасности, а второе касается синтаксической причуды, которая облегчает написание бесполезных инструкций assert.

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

 

Предостережение № 1: не используйте инструкции assert для проверки данных

Самое большое предостережение по поводу использования утверждений в Python состоит в том, что утверждения могут быть глобально отключены  переключателями командной строки -O и -OO, а также переменной окружения PYTHONOPTIMIZE в СPython.

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

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

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

Давайте взглянем на простой пример, который демонстрирует эту проблему. И снова представьте, что вы создаете приложение Python с интер­нет-магазином. Где-то среди программного кода вашего приложения есть функция, которая удаляет товар по запросу пользователя.

Поскольку вы только что узнали об assert, вам не терпится применить их в своем коде (я бы точно так поступил!), и вы пишете следующую реализацию:

def delete_product(prod_id, user):
   assert user.is_admin(), 'здесь должен быть администратор'
   assert store.has_product(prod_id), 'Неизвестный товар'
   store.get_product(prod_id).delete()

Приглядитесь поближе к функции delete_product. Итак, что же произой­дет, если инструкции assert будут отключены?

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

  1. Проверка полномочий администратора инструкциями assert несет в себе опасность. Если утверждения assert отключены в интерпрета­торе Python, то проверка полномочий превращается в нулевую опера­цию. И поэтому теперь любой пользователь может удалять товары. Проверка полномочий вообще не выполняется. В результате повы­шается вероятность того, что может возникнуть проблема, связанная с обеспечением безопасности, и откроется дверь для атак, способных разрушить или серьезно повредить данные в нашем интернет-магазине. Очень плохо.
  2. Проверка has_product() пропускается, когда assert отключена. Это означает, что метод get_product() теперь можно вызывать с недо­пустимыми идентификаторами товаров, что может привести к более серьезным ошибкам, — в зависимости от того, как написана наша программа. В худшем случае она может стать началом запуска DoS-атак. Например, если приложение магазина аварийно завершается при по­пытке стороннего лица удалить неизвестный товар, то, скорее всего, это произошло потому, что взломщик смог завалить его недопустимыми запросами на удаление и вызвать сбой в работе сервера.

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

def delete_product(product_id, user):
   if not user.is_admin():
      raise AuthError('Для удаления необходимы права админа')
   if not store.has_product(product_id):
      raise ValueError('Идентификатор неизвестного товара')
      store.get_product(product_id).delete()

 

Этот обновленный пример также обладает тем преимуществом, что вме­сто того, чтобы вызывать неопределенные исключения AssertionError, он теперь вызывает семантически правильные исключения, а имен­но ValueError или AuthError (которые мы должны были определить сами).

 

Предостережение № 2: инструкции assert, которые никогда не дают сбоя

Удивительно легко случайно написать инструкцию assert, которая всегда при вычислении возвращает истину. Мне самому в прошлом довелось по­нести ощутимый ущерб. Вкратце проблема в следующем.

Когда в инструкцию assert в качестве первого аргумента передается кортеж, assert всегда возвращает True и по этой причине выполняется успешно.

Например, это утверждение никогда не будет давать сбой: 

assert(1 == 2, 'Это утверждение должно вызвать сбой')

Эта ситуация связана с тем, что в Python непустые кортежи всегда явля­ются истинными. Если вы передаете кортеж в инструкцию assert, то это приводит к тому, что условие assert всегда будет истинным, что, в свою очередь, приводит к тому, что вышеупомянутая инструкция assert ста­нет бесполезной, потому что она никогда не сможет дать сбой и вызвать исключение.

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

assert (
   counter == 10,
      'Это должно было сосчитать все элементы'
)

 

На первый взгляд этот тестовый случай выглядит абсолютно приемле­мым. Однако он никогда не выловит неправильный результат: это ут­верждение assert всегда будет давать истину, независимо от состояния переменной counter. И в чем же тут дело? А в том, что оно подтверждает истинность объекта-кортежа.

Как я уже сказал, благодаря этому довольно легко выстрелить себе в ногу (моя все еще побаливает). Хорошая контрмера, с помощью которой можно избежать неприятностей от этой синтаксической причуды, — ис­пользовать линтер (linter), инструмент статического анализа кода. Кроме того, более свежие версии Python 3 для таких сомнительных инструкций assert показывают синтаксическое предупреждение.

Между прочим, именно поэтому вам также всегда следует выполнять быстрый тест «на дым» при помощи своих модульных тестовых случаев. Прежде чем переходить к написанию следующего, убедитесь, что они действительно не срабатывают.

 

Инструкции assert - резюме

Несмотря на данные выше предостережения, я полагаю, что инструкции assert являются мощным инструментом отладки, который зачастую не­достаточно используется разработчиками Python.

Понимание того, как работают инструкции assert и когда их применять, поможет писать программы Python, которые будет легче сопровождать и отлаживать.

Это великолепный навык, который стоит освоить, чтобы прокачать зна­ния  Python до более качественного уровня и стать всесторонним питонистом. Мне это позволило сэкономить бесконечные часы, которые приходилось тратить на отладку.

 

Ключевые выводы

 

Вас заинтересует / Intresting for you:

Top-7 IDE для Python-разработк...
Top-7 IDE для Python-разработк... 5819 просмотров Doctor Thu, 06 May 2021, 08:39:14
Создание приложения Android из...
Создание приложения Android из... 3343 просмотров Илья Дергунов Mon, 05 Nov 2018, 10:33:53
Байт-код Java: чудеса или реал...
Байт-код Java: чудеса или реал... 4681 просмотров Дэн Mon, 05 Nov 2018, 10:54:09
Пример простейшей программы на...
Пример простейшей программы на... 5665 просмотров Александров Попков Tue, 25 Sep 2018, 16:08:43
Печать
Войдите чтобы комментировать

apv аватар
apv ответил в теме #9599 4 года 2 нед. назад
Грамотная статья. Хорошие примеры. Архи-полезно!)
borisen аватар
borisen ответил в теме #9448 4 года 6 мес. назад
Согласен, высший пилотаж программирования. Думай, как говориться, головой, а не задом)
Daniil PR аватар
Daniil PR ответил в теме #9445 4 года 6 мес. назад
Очень толкова написано. Автор - молодец!