Лучшие практики использования assert?
Проблема:
- Существует ли проблема производительности или поддерживаемости кода при использовании
assert
в стандартном коде, а не только для отладки?
Является ли следующий код:
assert x >= 0, 'x is less than zero'
лучше или хуже, чем:
if x < 0:
raise Exception('x is less than zero')
- Также, существует ли способ установить бизнес-правило, например,
if x < 0 raise error
, которое всегда проверяется без использованияtry/except/finally
, чтобы в любое время в коде, еслиx
оказывается меньше 0, возникало исключение. Например, если я устанавливаюassert x < 0
в начале функции, чтобы в любой точке внутри функции, гдеx
становится меньше 0, вызывалось исключение?
5 ответ(ов)
Ассерты следует использовать для проверки условий, которые никогда не должны происходить. Цель заключается в том, чтобы выявить ошибку на раннем этапе в случае поврежденного состояния программы.
Исключения следует использовать для ошибок, которые потенциально могут произойти, и вы почти всегда должны создавать свои собственные классы исключений.
Например, если вы пишете функцию для чтения из конфигурационного файла и загрузки данных в словарь (dict
), неправильный формат в файле должен вызывать ConfigurationSyntaxError
, тогда как вы можете использовать assert
, чтобы проверить, что не собираетесь возвращать None
.
В вашем примере, если x
устанавливается через пользовательский интерфейс или из внешнего источника, тогда лучше использовать исключение.
Если x
устанавливается только вашим собственным кодом в той же программе, используйте ассерты.
Четыре назначения assert
Представьте, что вы работаете с кодом объемом 200,000 строк вместе с четырьмя коллегами: Элис, Берндом, Карлом и Дафной.
Каждый из вас вызывает код других, и вот в таких условиях assert
выполняет четыре роли:
- Сообщает Элис, Бернду, Карлу и Дафне, что ваш код ожидает.
Предположим, у вас есть метод, который обрабатывает список кортежей, и логика программы может сломаться, если эти кортежи не являются неизменяемыми:
```python
def mymethod(listOfTuples):
assert(all(type(tp) == tuple for tp in listOfTuples))
```
Это более надежно, чем аналогичная информация в документации, и гораздо проще поддерживать.
- Сообщает компьютеру, что ваш код ожидает.
assert
обеспечивает правильное поведение со стороны вызывающих ваш код. Если ваш код вызывает функции Элис, а затем Бернд вызывает ваш код, тогда без assert
, если программа сработает с ошибкой в коде Элис, Бернд может предположить, что это была ошибка Элис. Элис начинает разбираться и может подумать, что это ваша ошибка, затем вы исследуете ситуацию и сообщаете Бернду, что на самом деле это была его ошибка. В итоге — масса потраченного времени.
С использованием assert
, любой, кто неправильно вызовет функцию, быстро поймет, что это его ошибка, а не ваша. Все выиграют: Элис, Бернд и вы. Это экономит огромное количество времени.
- Сообщает читателям вашего кода (включая вас самих), что ваш код успешно выполнил какую-то задачу.
Допустим, у вас есть список записей, и каждая из них может быть чистой (что хорошо) или сморшенной, трале, гуллап или твинки. Если это сморша, она должна быть "несморшена"; если это трале, она должна быть "балудирована"; если это гуллап, она должна быть "троплена" (и, возможно, "шагала" потом); если она твинки, ее нужно "твикнуть" снова, кроме четвергов. Вы понимаете, да? Это сложные вещи. Но конечный результат (или должен быть) — все записи чистые. Правильным действием будет подвести итог результату вашего цикла очистки, например так:
```python
assert(all(entry.isClean() for entry in mylist))
```
Эта строка избавит всех от головной боли, пытаясь понять, что именно достоверно достигает этот замечательный цикл. И наиболее частым читателем, вероятно, будете вы сами.
- Сообщает компьютеру, что ваш код достиг определенного результата.
Если вы когда-либо забудете "шагать" запись, которая в этом нуждается, после "тропления", то assert
спасет ваш день и предотвратит поломку кода Дафны позже.
В моем понимании, у assert
есть две важные цели, связанные с документацией (1 и 3) и обеспечением надежности (2 и 4), которые равнозначны.
Информирование людей может оказаться даже более ценным, чем информирование компьютера, потому что это может предотвратить самые ошибки, которые assert
и призван поймать (в случае 1), а также множество последующих ошибок в любом случае.
В дополнение к другим ответам, сами операторы assert выбрасывают исключения, но только AssertionError. С утилитарной точки зрения, утверждения не подходят для случаев, когда вам нужно более детальное управление теми исключениями, которые вы перехватываете.
Единственная серьезная проблема с данным подходом заключается в том, что с помощью операторов assert сложно создавать понятные исключения. Если вы ищете более простой синтаксис, помните, что можно сделать что-то вроде этого:
class XLessThanZeroException(Exception):
pass
def CheckX(x):
if x < 0:
raise XLessThanZeroException()
def foo(x):
CheckX(x)
# здесь выполняем действия
Еще одна проблема заключается в том, что использование assert для обычной проверки условий затрудняет отключение проверок отладочных утверждений с помощью ключа -O.
Как было сказано ранее, утверждения (assertions) следует использовать, когда код не должен никогда достигать определенной точки, что свидетельствует о наличии ошибки. Вероятно, самым полезным приложением утверждений я вижу использование инвариантов, пред- и постусловий. Это условия, которые должны быть истинными в начале или в конце каждой итерации цикла или функции.
Например, в рекурсивной функции (в данном случае используются две отдельные функции, чтобы одна обрабатывала некорректный ввод, а другая — некорректный код, так как с рекурсией сложно проводить различие). Это позволит явно увидеть, если я забыл написать условие if, и понять, в чем была ошибка.
def SumToN(n):
if n <= 0:
raise ValueError("N must be greater than or equal to 0")
else:
return RecursiveSum(n)
def RecursiveSum(n):
# предусловие: n >= 0
assert(n >= 0)
if n == 0:
return 0
return RecursiveSum(n - 1) + n
# постусловие: возвращенная сумма от 1 до n
Эти инварианты циклов часто можно представить с помощью утверждений.
Как клонировать список, чтобы он не изменялся неожиданно после присваивания?
Преобразование списка словарей в DataFrame pandas
Ошибка: "'dict' объект не имеет метода 'iteritems'"
Как явно освободить память в Python?
Выбор строки из pandas Series/DataFrame по целочисленному индексу