Лучшая практика для Python 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, исключение возбуждается?
16 ответов
Чтобы можно было автоматически выдавать ошибку, когда x становится меньше нуля во всей функции. Вы можете использовать дескрипторы классов. Вот пример:
class LessThanZeroException(Exception):
pass
class variable(object):
def __init__(self, value=0):
self.__x = value
def __set__(self, obj, value):
if value < 0:
raise LessThanZeroException('x is less than zero')
self.__x = value
def __get__(self, obj, objType):
return self.__x
class MyClass(object):
x = variable()
>>> m = MyClass()
>>> m.x = 10
>>> m.x -= 20
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "my.py", line 7, in __set__
raise LessThanZeroException('x is less than zero')
LessThanZeroException: x is less than zero
Утверждения должны использоваться для проверки условий, которые никогда не должны возникать. Цель состоит в том, чтобы завершить работу рано в случае поврежденного состояния программы.
Исключения следует использовать для возможных ошибок, и вы почти всегда должны создавать свои собственные классы исключений.
Например, если вы пишете функцию для чтения из файла конфигурации в dict
неправильное форматирование в файле должно вызвать ConfigurationSyntaxError
в то время как вы можете assert
что ты не собираешься возвращаться None
,
В вашем примере, если x
это значение, установленное через пользовательский интерфейс или из внешнего источника, лучше всего исключение.
Если x
устанавливается только вашим собственным кодом в той же программе, следуйте утверждению.
Операторы assert удаляются, когда компиляция оптимизирована. Так что, да, есть как производительность, так и функциональные различия.
Текущий генератор кода не генерирует код для оператора assert, когда во время компиляции запрашивается оптимизация. - Python 2.6.4 Документация
Если вы используете assert
чтобы реализовать функциональность приложения, а затем оптимизировать развертывание в рабочей среде, вас будут преследовать дефекты "но это работает в dev".
Смотрите ПИТОНОПТИМИЗАЦИЯ и -O -OO
Четыре цели assert
Предположим, вы работаете над 200000 строк кода с четырьмя коллегами - Алисой, Берндом, Карлом и Дафни. Они называют ваш код, вы называете их код.
затем assert
имеет четыре роли:
Сообщите Алисе, Бернду, Карлу и Дафни, что ожидает ваш код.
Предположим, у вас есть метод, который обрабатывает список кортежей, и логика программы может нарушиться, если эти кортежи не являются неизменяемыми:def mymethod(listOfTuples): assert(all(type(tp)==tuple for tp in listOfTuples))
Это более достоверно, чем эквивалентная информация в документации, и намного проще в обслуживании.
Сообщите компьютеру, что ожидает ваш код.
assert
обеспечивает правильное поведение от вызывающих вашего кода. Если ваш код называет код Аликса и Бернда вашим, то безassert
Если в программе происходит сбой в коде Алисы, Бернд может предположить, что это вина Алисы, Алиса расследует и может предположить, что это ваша ошибка, вы расследуете и говорите Бернду, что это на самом деле его. Много работы потеряно.
С утверждениями, кто бы ни получил неправильный звонок, они быстро смогут увидеть, что это их вина, а не ваша. Алиса, Бернд, и вам всем это выгодно. Экономит огромное количество времени.Сообщите читателям своего кода (включая себя), чего достиг ваш код в какой-то момент.
Предположим, у вас есть список записей, и каждая из них может быть чистой (что хорошо) или может быть smorsh, trale, gullup или twinkled (что все неприемлемо). Если это сморш, то должно быть безмятежно; если это trale, это должно быть baludoed; если это гуллуп, то нужно идти рысью (и затем, возможно, тоже ходить); если он мерцает, он должен мигать снова, кроме четверга. Вы поняли: это сложные вещи. Но конечным результатом является (или должно быть), что все записи чистые. Правильно (TM) сделать, чтобы суммировать эффект вашей очистки цикла какassert(all(entry.isClean() for entry in mylist))
Это утверждение избавляет от головной боли всех, кто пытается понять, что же такое замечательный цикл. И наиболее частыми из этих людей, вероятно, будут вы сами.
Сообщите компьютеру, чего достиг ваш код в какой-то момент.
Если вы когда-нибудь забудете пройти запись, нуждающуюся в ней после рыси,assert
сэкономит ваш день и позволит избежать того, что ваш код сломается, дорогая Дафни намного позже.
В моей голове, assert
Две цели документации (1 и 3) и гарантия (2 и 4) одинаково ценны.
Информирование людей может быть даже более ценным, чем информирование компьютера, потому что это может предотвратить самые ошибки assert
стремится поймать (в случае 1) и множество последующих ошибок в любом случае.
В дополнение к другим ответам, сами утверждения выдают исключения, но только ошибки AssertionErrors. С утилитарной точки зрения утверждения не подходят для случаев, когда вам нужен точный контроль зерна, над которыми вы ловите исключения.
Единственное, что на самом деле не так с этим подходом, это то, что трудно сделать очень описательное исключение, используя операторы assert. Если вы ищете более простой синтаксис, помните, что вы также можете сделать что-то вроде этого:
class XLessThanZeroException(Exception):
pass
def CheckX(x):
if x < 0:
raise XLessThanZeroException()
def foo(x):
CheckX(x)
#do stuff here
Другая проблема заключается в том, что использование assert для обычной проверки условий состоит в том, что затрудняет отключение отладочных утверждений с помощью флага -O.
Слово " утверждать" на английском языке используется в смысле ругаться, утверждать, заявлять. Это не значит "проверить" или "должно быть". Это означает, что вы как кодер делаете здесь присяжные заявления:
# I solemnly swear that here I will tell the truth, the whole truth,
# and nothing but the truth, under pains and penalties of perjury, so help me FSM
assert answer == 42
Если код правильный, за исключением сбоев одного события, сбоев оборудования и т. Д., Никакое утверждение никогда не будет неудачным. Вот почему поведение программы для конечного пользователя не должно быть затронуто. Особенно утверждение не может потерпеть неудачу даже в исключительных программных условиях. Такого просто не бывает. Если это произойдет, программист должен быть зарезан за это.
Как было сказано ранее, утверждения следует использовать, когда ваш код НЕ ДОЛЖЕН когда-либо достигать точки, то есть там есть ошибка. Вероятно, наиболее полезная причина, по которой я вижу использование утверждения, - это инвариант / пред / постусловие. Это то, что должно быть истинно в начале или в конце каждой итерации цикла или функции.
Например, рекурсивная функция (2 отдельные функции, одна из которых обрабатывает неверный ввод, а другая - неверный код, поэтому трудно отличить от рекурсии). Это сделало бы очевидным, если бы я забыл написать заявление 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):
#precondition: n >= 0
assert(n >= 0)
if n == 0:
return 0
return RecursiveSum(n - 1) + n
#postcondition: returned sum of 1 to n
Эти инварианты цикла часто могут быть представлены с утверждением.
Если вы имеете дело с устаревшим кодом, который опирается на assert
чтобы функционировать должным образом, даже если это не так, добавление следующего кода будет быстрым решением, пока вы не найдете время для рефакторинга:
try:
assert False
raise Exception('Python Assertions are not working. This tool relies on Python Assertions to do its job. Possible causes are running with the "-O" flag or running a precompiled (".pyo" or ".pyc") module.')
except AssertionError:
pass
Что ж, это открытый вопрос, и у меня есть два аспекта, которые я хочу затронуть: когда добавлять утверждения и как писать сообщения об ошибках.
Цель
Чтобы объяснить это новичку, утверждения - это утверждения, которые могут вызывать ошибки, но вы их не уловите. И их обычно не следует растить, но в реальной жизни их все равно иногда поднимают. И это серьезная ситуация, от которой код не может справиться, что мы называем "фатальной ошибкой".
Далее, это для "целей отладки", что, хотя и правильно, звучит очень пренебрежительно. Мне больше нравится формулировка "объявлять инварианты, которые никогда не должны нарушаться", хотя она работает по-разному у разных новичков... Некоторые "просто поймут", а другие либо не находят ей применения, либо заменяют обычные исключения, или даже управлять потоком с его помощью.
Стиль
В Python assert
это утверждение, а не функция! (помнитьassert(False, 'is true')
не поднимет. Но, убрав это с дороги:
Когда и как писать необязательное "сообщение об ошибке"?
На самом деле это применимо к фреймворкам модульного тестирования, у которых часто есть много специальных методов для выполнения утверждений (assertTrue(condition)
, assertFalse(condition), assertEqual(actual, expected)
так далее.). Часто они также дают возможность прокомментировать утверждение.
В коде для выброса вы можете обойтись без сообщений об ошибках.
В некоторых случаях к утверждению нечего добавить:
def dump(something): assert isinstance(something, Dumpable) #...
Но помимо этого сообщение полезно для общения с другими программистами (которые иногда являются интерактивными пользователями вашего кода, например, в Ipython/Jupyter и т. Д.).
Предоставьте им информацию, а не только утечку внутренних деталей реализации.
вместо того:
assert meaningless_identifier <= MAGIC_NUMBER_XXX, 'meaningless_identifier is greater than MAGIC_NUMBER_XXX!!!'
записывать:
assert meaningless_identifier > MAGIC_NUMBER_XXX, 'reactor temperature above critical threshold'
а может даже:
assert meaningless_identifier > MAGIC_NUMBER_XXX, f'reactor temperature({meaningless_identifier }) above critical threshold ({MAGIC_NUMBER_XXX})'
Я знаю, я знаю - это не случай статического утверждения, но я хочу указать на информационную ценность сообщения.
Отрицательное или положительное сообщение?
Это может показаться спорным, но мне больно читать такие вещи, как:
assert a == b, 'a is not equal to b'
это две противоречивые вещи, написанные рядом друг с другом. Поэтому всякий раз, когда я оказываю влияние на кодовую базу, я настаиваю на указании того, что мы хотим, с помощью дополнительных глаголов, таких как "must" и "should", а не для того, чтобы сказать, чего мы не хотим.
assert a == b, 'a должно быть равно b'
Затем, получив AssertionError: a must be equal to b
также читается, и оператор выглядит логично в коде. Кроме того, вы можете получить что-то из этого, не читая трассировку (которая иногда может быть даже недоступна).
Есть ли проблема с производительностью?
Пожалуйста, не забудьте "заставить это работать сначала, прежде чем заставить это работать быстро".
Очень мало процентов любой программы обычно имеют отношение к ее скорости. Вы всегда можете выгнать или упроститьassert
если это когда-либо окажется проблемой производительности - и большинство из них никогда не будет.Будьте прагматичны:
Предположим, у вас есть метод, который обрабатывает непустой список кортежей, и логика программы сломается, если эти кортежи не являются неизменяемыми. Вы должны написать:def mymethod(listOfTuples): assert(all(type(tp)==tuple for tp in listOfTuples))
Это, вероятно, хорошо, если ваши списки имеют длину в десять записей, но это может стать проблемой, если в них есть миллион записей. Но вместо того, чтобы полностью отказаться от этого ценного чека, вы можете просто понизить его до
def mymethod(listOfTuples): assert(type(listOfTuples[0])==tuple) # in fact _all_ must be tuples!
это дешево, но, скорее всего, поймает большинство ошибок программы в любом случае.
Assert должен проверить -
1. действительное состояние,
2. действительное заявление,
3. истинная логика;
исходного кода. Вместо того, чтобы проваливать весь проект, он дает сигнал, что что-то не подходит в вашем исходном файле.
В примере 1, поскольку переменная 'str' не равна nul. Таким образом, никакие утверждения или исключения не будут подняты
Пример 1:
#!/usr/bin/python
str = 'hello Pyhton!'
strNull = 'string is Null'
if __debug__:
if not str: raise AssertionError(strNull)
print str
if __debug__:
print 'FileName '.ljust(30,'.'),(__name__)
print 'FilePath '.ljust(30,'.'),(__file__)
------------------------------------------------------
Output:
hello Pyhton!
FileName ..................... hello
FilePath ..................... C:/Python\hello.py
В примере 2 переменная 'str' имеет значение nul. Таким образом, мы спасаем пользователя от опережающей ошибочной программы оператором assert.
Пример 2:
#!/usr/bin/python
str = ''
strNull = 'NULL String'
if __debug__:
if not str: raise AssertionError(strNull)
print str
if __debug__:
print 'FileName '.ljust(30,'.'),(__name__)
print 'FilePath '.ljust(30,'.'),(__file__)
------------------------------------------------------
Output:
AssertionError: NULL String
В тот момент, когда мы не хотим отладки и осознали проблему утверждения в исходном коде. Отключить флаг оптимизации
python -O assertStatement.py
ничего не будет напечатано
Оба использования assert
и создание исключений связано с общением.
Утверждения - это утверждения о правильности кода, адресованные разработчикам: Утверждение в коде информирует читателей кода об условиях, которые должны быть выполнены для того, чтобы код был правильным. Утверждение, которое не выполняется во время выполнения, сообщает разработчикам, что в коде есть дефект, который необходимо исправить.
Исключения - это указания на нетипичные ситуации, которые могут возникнуть во время выполнения, но не могут быть разрешены имеющимся кодом, адресованным вызывающему коду, который там обрабатывается. Возникновение исключения не означает, что в коде есть ошибка.
Лучшая практика
Поэтому, если вы рассматриваете возникновение конкретной ситуации во время выполнения как ошибку, о которой вы хотели бы сообщить разработчикам ("Привет, разработчик, это условие означает, что где-то есть ошибка, исправьте код"), тогда пойти на утверждение. Если утверждение проверяет входные аргументы вашего кода, вам обычно следует добавить в документацию, что ваш код имеет "неопределенное поведение", когда входные аргументы нарушают эти условия.
Если вместо этого возникновение этой самой ситуации не является признаком ошибки в ваших глазах, а вместо этого является (возможно, редкой, но) возможной ситуацией, которая, по вашему мнению, должна обрабатываться клиентским кодом, вызовите исключение. Ситуации, когда возникает исключение, должны быть частью документации соответствующего кода.
Есть ли проблема с производительностью [...] при использовании
assert
Оценка утверждений занимает некоторое время. Однако их можно удалить во время компиляции. Однако это имеет некоторые последствия, см. Ниже.
Есть ли [...] проблема с обслуживанием кода при использовании
assert
Обычно утверждения улучшают ремонтопригодность кода, поскольку они улучшают удобочитаемость, делая предположения явными и регулярно проверяя эти предположения во время выполнения. Это также поможет выявить регрессии. Однако есть одна проблема, о которой следует помнить: выражения, используемые в утверждениях, не должны иметь побочных эффектов. Как упоминалось выше, утверждения могут быть исключены во время компиляции - это означает, что также исчезнут потенциальные побочные эффекты. Это может - непреднамеренно - изменить поведение кода.
Существует инфраструктура под названием JBoss Drools для Java, которая выполняет мониторинг времени выполнения для утверждения бизнес-правил, что отвечает на вторую часть вашего вопроса. Тем не менее, я не уверен, есть ли такая основа для Python.
В IDE, таких как PTVS, PyCharm, Wing assert isinstance()
операторы могут использоваться для включения завершения кода для некоторых неясных объектов.
Я бы добавил, что часто использую
assert
чтобы указать такие свойства, как инварианты цикла или логические свойства, которые должен иметь мой код, так же, как я бы указывал их в официально проверенном программном обеспечении.
Они служат как цели информирования читателей, помогая мне рассуждать, так и проверки того, что я не ошибаюсь в своих рассуждениях. Например :
k = 0
for i in range(n):
assert k == i * (i + 1) // 2
k += i
#do some things
или в более сложных ситуациях:
def sorted(l):
return all(l1 <= l2 for l1, l2 in zip(l, l[1:]))
def mergesort(l):
if len(l) < 2: #python 3.10 will have match - case for this instead of checking length
return l
k = len(l // 2)
l1 = mergesort(l[:k])
l2 = mergesort(l[k:])
assert sorted(l1) # here the asserts allow me to explicit what properties my code should have
assert sorted(l2) # I expect them to be disabled in a production build
return merge(l1, l2)