assert в Python

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

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

Пример использования assert

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

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

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

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

shoes = {'name': 'shoes', 'price': 14999}
1

Избегая проблем с округлением денежной цены, используйте целое число для представления цены в копейках. Итак, если к этим туфлям мы применим 25 %-ную скидку, то ожидаемо придем к отпускной цене 112,49:

apply_discount(shoes, 0.25)     # 112,49
1

Функция сработала. Теперь попробуем при­менить несколько недопустимых скидок. Например, 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
1
2
3
4
5
6
7

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

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

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

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

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

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

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

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

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

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

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

инструкция_assert ::= "assert" logical_expression ["," error_message]
1
  • logical_expression — это условие, которое мы проверяем,
  • error_message(не­обязательное) — это сообщение об ошибке, которое выводится на экран, если утверждение дает сбой.

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

if __debug__:
   if not logical_expression:
      raise AssertionError(error_message)
1
2
3

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

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

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

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

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

Ловушки assert

Есть два важных предостережения, на которые стоит обратить внимание:

  1. Первое из них связано с внесением в приложения ошибок и рисков, свя­занных с нарушением безопасности.
  2. Второе касается синтаксической причуды, которая облегчает написание бесполезных инструкций 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()
1
2
3
4

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

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

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

  • Проверка 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()
1
2
3
4
5
6

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

WARNING

Инструкции assert, которые никогда не дают сбоя.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Инструкции assert могут быть глобально отключены в настройках интерпретатора.