Оператор Python assert — это один из инструментов, который доступен вам как разработчику Python для повышения надежности ваших программ.
Что такое оператор assert в Python?
Оператор assert позволяет проверить, что состояние программы Python соответствует ожидаемому разработчиком. Выражения, проверенные с помощью assert, всегда должны быть истинными, если только в программе нет неожиданной ошибки.
В этой статье мы узнаем, как использовать оператор assert в приложениях Python.
Давайте начнем!
1. Практикуйте очень простой пример утверждения assert
Чтобы показать вам, как работает assert, мы начнем с простого выражения, использующего assert (также называемого assertion).
Откройте терминал и введите Python, чтобы открыть интерактивную оболочку Python:
$ python
Python 3.7.4 (default, Aug 13 2019, 15:17:50)
[Clang 4.0.1 (tags/RELEASE_401/final)]:: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
Выполните следующее выражение, которое является логически истинным:
>>> assert 5>0
Как видите, ничего не происходит…
Теперь выполните другое выражение, которое логически ложно:
>>> assert 5<0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
На этот раз мы видим исключение, вызванное интерпретатором Python, и это исключение имеет тип AssertionError.
Из этого примера мы можем определить поведение оператора assert.
Оператор assert проверяет логическое условие. Если условие истинно, выполнение программы продолжается. Если условие ложно, assert вызывает AssertionError.
В этом случае возвращаемая нами ошибка не очень ясна…
…что если мы хотим, чтобы AssertError также предоставлял сообщение, объясняющее тип ошибки?
Для этого мы можем передать необязательное сообщение в оператор assert.
assert <condition>, <optional message>
Попробуйте обновить предыдущее выражение следующим сообщением:
>>> assert 5<0, "The number 5 is not negative"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: The number 5 is not negative
На этот раз мы получаем исключение, которое четко объясняет причину ошибки.
2. Сравните Assert и Raise в Python
Чтобы дать вам полное представление о том, как ведет себя оператор assert, мы рассмотрим альтернативный код, использующий raise, который ведет себя так же, как assert.
if __debug__
if not <condition>:
raise AssertionError(<message>)
Давайте применим это к нашему примеру:
if __debug__:
if not 5<0:
raise AssertionError("The number 5 is not negative")
Как вы можете видеть ниже, поведение идентично поведению утверждения, которое мы видели в предыдущем разделе:
>>> if __debug__:
... if not 5<0:
... raise AssertionError("The number 5 is not negative")
...
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
AssertionError: The number 5 is not negative
Вторая и третья строки нашего кода вполне понятны.
А как насчет первой строки? Что такое __debug__
?
Давайте посмотрим, сможет ли оболочка Python ответить на этот вопрос:
>>> __debug__
True
Интересно, так __debug__
— это True. Это объясняет, почему выполняются вторая и третья строки нашего кода.
Но это все еще не говорит нам, что такое __debug__
…
Это не то, что мы определили. Это означает, что это то, что предоставляется Python как языком из коробки.
Согласно документации Python, __debug__
— это встроенная константа, которая имеет значение true, если вы не запускаете Python с флагом -O
.
Давайте выясним, так ли это на самом деле…
Откройте новую интерактивную оболочку Python с помощью параметра -O:
$ python -O
Python 3.7.4 (default, Aug 13 2019, 15:17:50)
[Clang 4.0.1 (tags/RELEASE_401/final)]:: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> __debug__
False
На этот раз константа __debug__
равна False. И если мы запустим предыдущий код…
>>> if __debug__:
... if not 5<0:
... raise AssertionError("The number 5 is not negative")
...
>>>
Ничего не происходит. Очевидно, потому что первое условие if ложно.
3. Как отключить утверждения в Python
В предыдущем разделе я дал вам подсказку о том, как можно отключить утверждения в Python.
Давайте воспользуемся другим примером, чтобы объяснить это…
Создайте новую программу Python с именем assert_example.py, содержащую следующий код:
month = "January"
assert type(month) == str
print("The month is {}".format(month))
Мы используем утверждение, чтобы убедиться, что переменная month имеет тип String, а затем выводим сообщение.
Что произойдет, если значение переменной month не является строкой?
month = 2021
Получаем следующий вывод:
$ python assert_example.py
Traceback (most recent call last):
File "assert_example.py", line 2, in <module>
assert type(month) == str
AssertionError
Как и ожидалось, мы получаем AssertionError.
А теперь…
…мы добавляем флаг -O при запуске программы:
$ python -O assert_example.py
The month is 2021
Интересно и страшно одновременно!
Оператор assert не был вызван или, точнее, был отключен.
Флаг Python -O отключает выполнение операторов assert в вашей программе Python. Обычно это делается из соображений производительности при создании сборки релиза для развертывания в производственных системах.
В этом случае вы можете видеть, что отсутствующая логика утверждения привела к ошибке в нашей программе, которая просто предполагает, что месяц указан в правильном формате.
В следующем разделе мы увидим, почему это не обязательно правильный способ использования assert.
4. Не проверяйте входные данные с помощью Assert
В предыдущем примере мы увидели, как отключение утверждений привело к некорректному поведению нашей программы.
Это именно то, чего не должно происходить, если вы отключите утверждения. Оператор утверждения предназначен для проверки условий, которые никогда не должны возникать, а не для изменения логики вашей программы.
Поведение вашей программы не должно зависеть от утверждений, и вы должны иметь возможность удалять их, не меняя способ работы вашей программы.
Вот очень важное правило, которому следует следовать…
Не используйте оператор assert для проверки ввода пользователя.
Давайте разберемся почему…
Создайте программу, которая считывает число, используя функцию ввода:
number = int(input("Insert a number: "))
assert number>0, "The number must be greater than zero"
output = 100/number
print(output)
Мы используем оператор assert, чтобы убедиться, что число положительное, а затем вычисляем результат как 100, деленное на наше число.
$ python assert_example.py
Insert a number: 4
25.0
Теперь попробуем передать ноль в качестве входных данных:
$ python assert_example.py
Insert a number: 0
Traceback (most recent call last):
File "assert_example.py", line 2, in <module>
assert number>0, "The number must be greater than zero"
AssertionError: The number must be greater than zero
Как и ожидалось, оператор assert вызывает исключение AssertionError, поскольку его условие ложно.
Теперь давайте выполним Python с флагом -O, чтобы отключить утверждения:
$ python -O assert_example.py
Insert a number: 0
Traceback (most recent call last):
File "assert_example.py", line 3, in <module>
output = 100/number
ZeroDivisionError: division by zero
На этот раз утверждение не выполняется, и наша программа пытается разделить 100 на ноль, что приводит к исключению ZeroDivisionError.
Теперь понятно, почему не следует проверять вводимые пользователем данные с помощью утверждений. Потому что утверждения можно отключить, и в этом случае любая проверка с помощью утверждений будет пропущена.
Очень опасно, это может вызвать любые проблемы с безопасностью в вашей программе.
Представьте, что бы произошло, если бы вы использовали оператор assert для проверки того, имеет ли пользователь права на обновление данных в вашем приложении. А затем эти утверждения отключены в Production.
5. Проверьте условия, которые никогда не должны возникать
В этом разделе мы увидим, как утверждения могут помочь нам быстрее находить причины ошибок.
Мы будем использовать assert, чтобы убедиться, что определенное условие не возникнет. Если оно возникнет, то где-то в нашем коде есть ошибка.
Давайте рассмотрим один пример с функцией Python, которая вычисляет площадь прямоугольника:
def calculate_area(length, width):
area = length*width
return area
length = int(input("Insert the length: "))
width = int(input("Insert the width: "))
print("The area of the rectangle is {}".format(calculate_area(length, width)))
При запуске этой программы я получаю следующий вывод:
$ python assert_example.py
Insert the length: 4
Insert the width: 5
The area of the rectangle is 20
Теперь посмотрим, что произойдет, если я передам отрицательную длину:
$ python assert_example.py
Insert the length: -4
Insert the width: 5
The area of the rectangle is -20
Это на самом деле не имеет смысла…
…площадь не может быть отрицательной!
Так что же мы можем с этим поделать? Это состояние, которое никогда не должно возникать.
Давайте посмотрим, как assert может помочь:
def calculate_area(length, width):
area = length*width
assert area>0, "The area of a rectangle cannot be negative"
return area
Я добавил утверждение, которое проверяет, что площадь положительна.
$ python assert_example.py
Insert the length: -4
Insert the width: 5
Traceback (most recent call last):
File "assert_example.py", line 8, in <module>
print("The area of the rectangle is {}".format(calculate_area(length, width)))
File "assert_example.py", line 3, in calculate_area
assert area>0, "The area of a rectangle cannot be negative"
AssertionError: The area of a rectangle cannot be negative
Как и ожидалось, мы получаем исключение AssertionError, поскольку значение площади отрицательное.
Это останавливает выполнение нашей программы, предотвращая потенциальное использование этого значения в других операциях.
6. Скобки и утверждения в Python
Если вы используете Python 3, вы можете задаться вопросом, почему в предыдущих примерах assert мы никогда не использовали скобки после assert.
Например, как вы знаете, в Python 3 оператор печати пишется следующим образом:
print("Message you want to print")
Так почему же то же самое не относится к утверждениям?
Давайте посмотрим, что произойдет, если мы возьмем предыдущее выражение assert и заключим его в скобки:
>>> number = 0
>>> assert(number>0, "The number must be greater than zero")
Попробуйте запустить этот оператор assert в оболочке Python. Вы получите следующую ошибку:
<stdin>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?
Но почему ошибка гласит, что утверждение всегда истинно?
Это связано с тем, что после добавления скобок условие утверждения стало кортежем.
Формат кортежа — (значение1, значение2, …, значениеN), а кортеж в логическом контексте всегда имеет значение True, если только он не содержит никаких значений.
Вот что произойдет, если мы передадим пустой кортеж в качестве оператора assert:
>>> assert()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
Python вызывает исключение AssertionError, поскольку пустой кортеж всегда является ложным.
Вот как можно проверить, как кортеж оценивается как логическое значение:
>>> number = 0
>>> print(bool((number>0, "The number must be greater than zero")))
True
>>> print(bool(()))
False
Пустой кортеж транслируется в False в булевом контексте. Непустой кортеж транслируется в True.
Ниже вы можете увидеть правильный способ использования скобок с assert:
>>> number = 0
>>> assert(number>0), "The number must be greater than zero"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: The number must be greater than zero
Поэтому не забывайте быть осторожными при использовании скобок с assert, чтобы избежать ошибок, вызванных тем фактом, что вы можете подумать, что ваше assert верно, хотя на самом деле оно не выполняет ожидаемую вами проверку.
7. Утверждение Assert в модульных тестах
Операторы Assert также используются в модульных тестах для проверки того, что результат, возвращаемый определенной функцией, соответствует ожидаемому.
Я хочу написать модульные тесты для нашей функции, которая вычисляет площадь прямоугольника (без включения оператора assert в функцию).
На этот раз мы передадим аргументы через командную строку вместо того, чтобы запрашивать длину и ширину в интерактивном режиме:
import sys
def calculate_area(length, width):
area = length*width
return area
def main(length, width):
print("The area of the rectangle is {}".format(calculate_area(length, width)))
if __name__ == '__main__':
length = int(sys.argv[1])
width = int(sys.argv[2])
main(length, width)
Вывод:
$ python assert_example.py 4 5
The area of the rectangle is 20
Для начала давайте создадим тестовый класс…
Не беспокойтесь о каждой отдельной детали этого класса, если вы никогда раньше не писали модульные тесты на Python. Основная концепция здесь заключается в том, что мы можем использовать операторы assert для выполнения автоматизированных тестов.
Давайте напишем простой тестовый пример успешного выполнения.
import unittest
from assert_example import calculate_area
class TestAssertExample(unittest.TestCase):
def test_calculate_area_success(self):
length = 4
width = 5
area = calculate_area(length, width)
self.assertEqual(area, 20)
if __name__ == '__main__':
unittest.main()
Как видите, мы импортируем unittest, а также функцию calculate_area из assert_example.py.
Затем мы определяем класс TestAssertExample, который наследует другой класс, unittest.TestCase.
Наконец, мы создаем метод test_calculate_area_success, который вычисляет площадь и проверяет, что ее значение соответствует ожидаемому, используя оператор assertEqual.
Это немного другой тип утверждения по сравнению с тем, что мы видели до сих пор. Существует несколько типов методов утверждения, которые вы можете использовать в своих модульных тестах Python в зависимости от того, что вам нужно.
Давайте выполним модульный тест:
$ python test_assert_example.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Тест пройден успешно.
Что делать, если я хочу проверить негативный сценарий?
Я хочу убедиться, что моя функция выдает исключение, если хотя бы одно из значений length и width отрицательно. Я могу добавить следующий тестовый метод в наш тестовый класс, а для проверки исключений мы можем использовать метод assertRaises:
def test_calculate_area_failure(self):
length = -4
width = 5
self.assertRaises(ValueError, calculate_area, length, width)
Давайте выясним, были ли оба теста успешными…
$ python test_assert_example.py
F.
======================================================================
FAIL: test_calculate_area_failure (__main__.TestAssertExample)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_assert_example.py", line 15, in test_calculate_area_failure
self.assertRaises(TypeError, calculate_area, length, width)
AssertionError: ValueError not raised by calculate_area
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
Хм, это не так!
Одна ошибка вызвана тем, что метод calculate_area не вызывает исключение ValueError, если длина или ширина отрицательны.
Пришло время улучшить нашу функцию для обработки этого сценария:
def calculate_area(length, width):
if length < 0 or width < 0:
raise ValueError("Length and width cannot be negative")
area = length*width
return area
А теперь давайте снова запустим оба модульных теста:
$ python test_assert_example.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
На этот раз все хорошо 🙂
Ниже вы можете найти полный набор тестов:
import unittest
from assert_example import calculate_area
class TestAssertExample(unittest.TestCase):
def test_calculate_area_success(self):
length = 4
width = 5
area = calculate_area(length, width)
self.assertEqual(area, 20)
def test_calculate_area_failure(self):
length = -4
width = 5
self.assertRaises(ValueError, calculate_area, length, width)
if __name__ == '__main__':
unittest.main()
Заключение
В этом уроке я познакомил вас с оператором assert в Python и показал, в чем разница между ним и комбинацией операторов if и raise.
Мы увидели, что в Python можно отключить утверждения, и это может быть полезно для повышения производительности производственных сборок.
Это также причина, по которой утверждения assert никогда не следует использовать для реализации логики приложения, такой как проверка ввода или проверки безопасности. Если их отключить, эти утверждения могут привести к критическим ошибкам в нашей программе.
Основная цель assert — гарантировать, что определенные условия, противоречащие поведению нашей программы, никогда не возникнут (например, переменная, которая всегда должна быть положительной, каким-то образом окажется отрицательной или переменная не попадет в ожидаемый диапазон).
Наконец, мы узнали, почему использование скобок в утверждениях может привести к ошибкам и почему концепция утверждений чрезвычайно важна, когда мы хотим разрабатывать модульные тесты, чтобы гарантировать надежность кода.
Какая часть этой статьи оказалась для вас наиболее полезной?