6

Лучшие практики использования assert?

12

Проблема:

  1. Существует ли проблема производительности или поддерживаемости кода при использовании assert в стандартном коде, а не только для отладки?
    Является ли следующий код:
assert x >= 0, 'x is less than zero'

лучше или хуже, чем:

if x < 0:
    raise Exception('x is less than zero')
  1. Также, существует ли способ установить бизнес-правило, например, if x < 0 raise error, которое всегда проверяется без использования try/except/finally, чтобы в любое время в коде, если x оказывается меньше 0, возникало исключение. Например, если я устанавливаю assert x < 0 в начале функции, чтобы в любой точке внутри функции, где x становится меньше 0, вызывалось исключение?

5 ответ(ов)

8

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

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


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


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

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

1

Четыре назначения assert

Представьте, что вы работаете с кодом объемом 200,000 строк вместе с четырьмя коллегами: Элис, Берндом, Карлом и Дафной.

Каждый из вас вызывает код других, и вот в таких условиях assert выполняет четыре роли:

  1. Сообщает Элис, Бернду, Карлу и Дафне, что ваш код ожидает.

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

```python
def mymethod(listOfTuples):
    assert(all(type(tp) == tuple for tp in listOfTuples))
```

Это более надежно, чем аналогичная информация в документации, и гораздо проще поддерживать.

  1. Сообщает компьютеру, что ваш код ожидает.

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

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

  1. Сообщает читателям вашего кода (включая вас самих), что ваш код успешно выполнил какую-то задачу.

Допустим, у вас есть список записей, и каждая из них может быть чистой (что хорошо) или сморшенной, трале, гуллап или твинки. Если это сморша, она должна быть "несморшена"; если это трале, она должна быть "балудирована"; если это гуллап, она должна быть "троплена" (и, возможно, "шагала" потом); если она твинки, ее нужно "твикнуть" снова, кроме четвергов. Вы понимаете, да? Это сложные вещи. Но конечный результат (или должен быть) — все записи чистые. Правильным действием будет подвести итог результату вашего цикла очистки, например так:

```python
assert(all(entry.isClean() for entry in mylist))
```

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

  1. Сообщает компьютеру, что ваш код достиг определенного результата.

Если вы когда-либо забудете "шагать" запись, которая в этом нуждается, после "тропления", то assert спасет ваш день и предотвратит поломку кода Дафны позже.

В моем понимании, у assert есть две важные цели, связанные с документацией (1 и 3) и обеспечением надежности (2 и 4), которые равнозначны.

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

0

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

0

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

class XLessThanZeroException(Exception):
    pass

def CheckX(x):
    if x < 0:
        raise XLessThanZeroException()

def foo(x):
    CheckX(x)
    # здесь выполняем действия

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

0

Как было сказано ранее, утверждения (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

Эти инварианты циклов часто можно представить с помощью утверждений.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь