Ошибки вычислений python

Время на прочтение
9 мин

Количество просмотров 35K

К старту курса по Fullstack-разработке на Python делимся решениями классической проблемы неточности чисел с плавающей точкой. В материале вы найдёте примеры работы с функциями и классами, предназначенными специально для решения проблем чисел с плавающей точкой.


Числа с плавающей точкой — быстрый и эффективный способ хранения чисел и работы с ними. Но он связан с рядом трудностей для начинающих и опытных программистов! Вот классический пример:

>>> 0.1 + 0.2 == 0.3
False

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

Компьютер обманывает вас

Вы видели, что 0.1 + 0.2 не равно 0.3, но безумие на этом не заканчивается. Вот ещё пара примеров, сбивающих с толку:

>>> 0.2 + 0.2 + 0.2 == 0.6
False

>>> 1.3 + 2.0 == 3.3
False

>>> 1.2 + 2.4 + 3.6 == 7.2
False

Проблема касается и сравнения:

>>> 0.1 + 0.2 <= 0.3
False

>>> 10.4 + 20.8 > 31.2
True

>>> 0.8 - 0.1 > 0.7
True

Что происходит? Когда вы вводите в интерпретатор Python число 0.1, оно сохраняется в памяти как число с плавающей точкой и происходит преобразование. 0.1 — это десятичное число с основанием 10, но числа с плавающей точкой хранятся в двоичной записи. То есть основание 0.1 преобразуется из 10 в 2.

Получающееся двоичное число может недостаточно  точно представлять исходное число с основанием 10. 0.1 — один из примеров. Двоичным представлением будет 0.0(0011). То есть 0.1 — это бесконечно повторяющееся десятичное число, записанное с основанием 2. То же происходит, когда в виде десятичного числа с основанием 10 записывается дробь ⅓. Получается бесконечно повторяющееся десятичное число 0.3(3).

Память компьютера конечна, поэтому бесконечно повторяющееся представление двоичной дроби 0.1 округляется до конечной дроби. Её значение зависит от архитектуры компьютера (32- или 64-разрядная).

Увидеть значение с плавающей точкой, которое сохраняется для 0.1, можно с помощью метода .as_integer_ratio(). Представление с плавающей точкой состоит из числителя и знаменателя:

>>> numerator, denominator = (0.1).as_integer_ratio()
>>> f"0.1 ≈ {numerator} / {denominator}"
'0.1 ≈ 3602879701896397 / 36028797018963968'

Чтобы отобразить дробь с точностью до 55 знаков после запятой, используем format():

>>> format(numerator / denominator, ".55f")
'0.1000000000000000055511151231257827021181583404541015625'

Так 0.1 округляется до числа чуть больше, чем его истинное значение.

Узнайте больше о числовых методах, подобных .as_integer_ratio(), в моей статье 3 Things You Might Not Know About Numbers in Python («3 факта о числах в Python, которые вы могли не знать»).

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

Ошибка представления числа очень типична

Существуют три причины, почему число округляется при его представлении в виде числа с плавающей точкой:

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

  2. Это иррациональное число.

  3. Число рациональное, но без конечного двоичного представления. 

64-разрядные числа с плавающей точкой имеют 16 или 17 значащих разрядов. Любое число, у которого значащих разрядов больше, округляется. Иррациональные числа, такие как π и e, нельзя представить конечной дробью с целочисленным основанием. И, опять же, иррациональные числа в любом случае округляются при сохранении в виде чисел с плавающей точкой.

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

А как на счёт бесконечных рациональных чисел, например 0.1 с основанием 2? Именно здесь вам встретится большинство связанных с плавающей точкой трудностей и благодаря математике — позволяющей определять, конечная дробь или нет, — вы будете сталкиваться с ошибками представления чаще, чем думаете.

С основанием 10 дробь можно представить как конечную, если её знаменатель — произведение степеней простых множителей 10. Два простых множителя 10 — это 2 и 5, поэтому дроби ½, ¼, ⅕, ⅛ и ⅒ — конечные, а ⅓, ⅐ и ⅑ — нет. У основания 2 только один простой множитель — 2.

Конечные дроби здесь только те, знаменатель которых равен степени числа 2. В результате дроби ⅓, ⅕, ⅙, ⅐, ⅑ и ⅒ — бесконечные, когда представлены в двоичной записи.

Теперь наш первый пример должен стать понятнее. 0.1, 0.2 и 0.3 при преобразовании в числа с плавающей точкой округляются:

>>> # -----------vvvv  Display with 17 significant digits
>>> format(0.1, ".17g")
'0.10000000000000001'

>>> format(0.2, ".17g")
'0.20000000000000001'

>>> format(0.3, ".17g")
'0.29999999999999999'

При сложении 0.1 и 0.2 получается число чуть больше 0.3:

>>> 0.1 + 0.2
0.30000000000000004

А поскольку 0.1 + 0.2 чуть больше, чем 0.3, и 0.3 представлено числом, которое чуть меньше 0.3, выражение 0.1 + 0.2 == 0.3 оказывается False.

Об ошибке представления чисел с плавающей точкой должен знать каждый программист на любом языке — и уметь с ней справляться. Она характерна не только для Python. Результат вывода 0.1 + 0.2 на разных языках можно увидеть на сайте с подходящим названием 0.30000000000000004.com.

Как сравнивать числа с плавающей точкой в Python

Как же справляться с ошибками представления чисел с плавающей точкой при сравнении таких чисел в Python? Хитрость заключается в том, чтобы избегать проверки на равенство. Вместо ==, >= или <= всегда используйте с числами с плавающей точкой функцию math.isclose():

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True

В math.isclose() проверяется, достаточно ли близок первый аргумент ко второму. То есть проверяется расстояние между двумя аргументами. Оно равно абсолютной величине разницы обоих значений:

>>> a = 0.1 + 0.2
>>> b = 0.3
>>> abs(a - b)
5.551115123125783e-17

Если abs(a — b) меньше некоего процента от большего значения a или b, то a считается достаточно близким к b, чтобы считаться «равным» b. Этот процент называется относительной погрешностью и указывается именованным аргументом rel_tol функции math.isclose(), который по умолчанию равен 1e-9.

То есть если abs(a — b) меньше 0.00000001 * max(abs(a), abs(b)), то a и b считаются «близкими» друг к другу. Это гарантирует, что a и b будут приблизительно с девятью знаками после запятой.

Если нужно, можно изменить относительную погрешность:

>>> math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-20)
False

Конечно, относительная погрешность зависит от ограничений задачи, но для большинства повседневных приложений стандартной относительной погрешности должно быть достаточно. Проблема возникает, если a или b равно нулю, а rel_tol меньше единицы. Тогда, как бы ни было близко ненулевое значение к нулю, относительной погрешностью гарантируется, что проверка на близость будет неудачной. В качестве запасного варианта здесь применяется абсолютная погрешность:

>>> # Relative check fails!
>>> # ---------------vvvv  Relative tolerance
>>> # ----------------------vvvvv  max(0, 1e-10)
>>> abs(0 - 1e-10) < 1e-9 * 1e-10
False

>>> # Absolute check works!
>>> # ---------------vvvv  Absolute tolerance
>>> abs(0 - 1e-10) < 1e-9
True

В math.isclose() эта проверка выполняется автоматически. Абсолютная погрешность определяется с помощью именованного аргумента abs_tol. Но abs_tol по умолчанию равен 0.0, поэтому придётся задать его вручную, если нужно проверить близость значения к нулю.

В итоге в функции math.isclose() возвращается результат следующего сравнения — с относительными и абсолютными проверками в одном выражении:

abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)

math.isclose() появилась в PEP 485 и доступна с Python 3.5.

Когда стоит использовать math.isclose()?

В целом math.isclose() следует применять, сравнивая значения с плавающей точкой. Заменим == на math.isclose():

>>> # Don't do this:
>>> 0.1 + 0.2 == 0.3
False

>>> # Do this instead:
>>> math.isclose(0.1 + 0.2, 0.3)
True

Со сравнениями >= и <= нужно быть осторожным. Обработаем равенство отдельно, используя math.isclose(), а затем проверим строгое сравнение:

>>> a, b, c = 0.1, 0.2, 0.3

>>> # Don't do this:
>>> a + b <= c
False

>>> # Do this instead:
>>> math.isclose(a + b, c) or (a + b < c)
True

Есть альтернативы math.isclose(). Если вы работаете с NumPy, можете использовать numpy.allclose() и numpy.isclose():

>>> import numpy as np

>>> # Use numpy.allclose() to check if two arrays are equal
>>> # to each other within a tolerance.
>>> np.allclose([1e10, 1e-7], [1.00001e10, 1e-8])
False

>>> np.allclose([1e10, 1e-8], [1.00001e10, 1e-9])
True

>>> # Use numpy.isclose() to check if the elements of two arrays
>>> # are equal to each other within a tolerance
>>> np.isclose([1e10, 1e-7], [1.00001e10, 1e-8])
array([ True, False])

>>> np.isclose([1e10, 1e-8], [1.00001e10, 1e-9])
array([ True, True])

Имейте в виду: стандартные относительные и абсолютные погрешности — не то же самое, что math.isclose(). Стандартная относительная погрешность для numpy.allclose() и numpy.isclose() равна 1e-05, а стандартная абсолютная погрешность — 1e-08.

math.isclose() особенно удобна для модульных тестов, хотя и здесь имеются альтернативы. Во встроенном модуле unittest в Python есть метод unittest.TestCase.assertAlmostEqual().

Но в нём применяется только проверка абсолютной разницы. А это ещё и утверждение, то есть при сбоях возникает ошибка AssertionError, из-за которой оно непригодно для сравнений в бизнес-логике.

Отличная альтернатива math.isclose() для модульного тестирования — это функция pytest.approx() из pytest pytest. Как и в math.isclose(), здесь принимаются два аргумента и возвращается, равны они или нет в пределах некой погрешности:

>>> import pytest
>>> 0.1 + 0.2 == pytest.approx(0.3)
True

Как и в math.isclose(), в pytest.approx() для задания относительной и абсолютной погрешностей есть именованные аргументы rel_tol и abs_tol. Но стандартные значения различаются. У rel_tol оно 1e-6, а у abs_tol — 1e-12.

Если первые переданные в pytest.approx() два аргумента подобны массиву (то есть это итерируемый объект Python, такой же, как список или кортеж или даже массив NumPy), тогда в pytest.approx() поведение подобно numpy.allclose() и возвращается то, равны эти два массива или нет в пределах погрешностей:

>>> import numpy as np                                                          
>>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == pytest.approx(np.array([0.3, 0.6])) 
True

Для pytest.approx() сгодятся даже значения словаря:

>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == pytest.approx({'a': 0.3, 'b': 0.6})
True

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

Точные альтернативы числам с плавающей точкой

В Python есть два встроенных числовых типа, которые обеспечивают полную точность в ситуациях, когда числа с плавающей точкой не подходят: Decimal и Fraction.

Тип Decimal

В типе Decimal могут храниться десятичные значения именно с той точностью, какая нужна. По умолчанию в нём сохраняются 28 значащих цифр (это число можно изменить согласно конкретной решаемой задаче):

>>> # Import the Decimal type from the decimal module
>>> from decimal import Decimal

>>> # Values are represented exactly so no rounding error occurs
>>> Decimal("0.1") + Decimal("0.2") == Decimal("0.3")
True

>>> # By default 28 significant figures are preserved
>>> Decimal(1) / Decimal(7)
Decimal('0.1428571428571428571428571429')

>>> # You can change the significant figures if needed
>>> from decimal import getcontext
>>> getcontext().prec = 6  # Use 6 significant figures
>>> Decimal(1) / Decimal(7)
Decimal('0.142857')

Больше узнать о типе Decimal можно в документации Python.

Тип Fraction

Альтернатива числам с плавающей точкой — тип Fraction. В нём могут точно сохраняться рациональные числа. При этом устраняются проблемы с ошибками представления, возникающие в числах с плавающей точкой:

>>> # import the Fraction type from the fractions module
>>> from fractions import Fraction

>>> # Instantiate a Fraction with a numerator and denominator
>>> Fraction(1, 10)
Fraction(1, 10)

>>> # Values are represented exactly so no rounding error occurs
>>> Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10)
True

У типов Fraction и Decimal много преимуществ по сравнению со стандартными значениями с плавающей точкой. Но есть и недостатки: меньшая скорость и повышенное потребление памяти.

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

Заключение

Значения с плавающей точкой — это и благо, и проклятие одновременно. Они обеспечивают быстрые арифметические операции и эффективное потребление памяти за счёт неточного представления. Из этой статьи вы узнали:

  • Почему числа с плавающей точкой неточны.

  • Почему ошибка представления с плавающей точкой типична.

  • Как корректно сравнивать значения с плавающей точкой.

  • Как точно представлять числа, используя типы Fraction и Decimal. 

Узнайте о числах в Python ещё больше. Например, знаете ли вы, что int — не единственный целочисленный тип в Python? Узнайте, какой есть ещё, а также о других малоизвестных фактах о числах в моей статье.

А мы поможем вам прокачать скиллы или с самого начала освоить профессию, востребованную в любое время:

  • Профессия Fullstack-разработчик на Python

  • Профессия Data Analyst

Выбрать другую востребованную профессию.

Краткий каталог курсов и профессий

Числа с плавающей точкой представлены в компьютерах в виде двоичных дробей. Например, десятичная дробь 0.125 имеет значение 1/10 + 2/100 + 5/1000, и таким же образом двоичная дробь 0.001 имеет значение 0/2 + 0/4 + 1/8. Эти две дроби имеют одинаковые значения, единственное реальное отличие состоит в том, что первая записана в дробной записи с основанием 10, а вторая — с основанием 2.

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

Сначала проблему легче понять из базы 10. Рассмотрим дробь 1/3. Вы можете приблизить это как основную 10 фракцию: 0.3 или лучше, 0.33или лучше, 0.333 и так далее. Независимо от того, сколько цифр вы хотите записать, результат никогда не будет ровно 1/3, но будет все более приближенным к 1/3.

Таким же образом, независимо от того, сколько цифр из 2-х оснований вы хотите использовать, десятичное значение 0,1 не может быть представлено в точности как дробь из 2-х оснований. В базе 2 1/10 — бесконечно повторяющаяся дробь 0.0001100110011001100110011...

Остановитесь на любом конечном количестве битов, и вы получите приближение. На большинстве современных машин числа с плавающей запятой аппроксимируются с использованием двоичной дроби, а числитель использует первые 53 бита, начиная с самого старшего бита, а знаменатель — как степень двух. В случае 1/10 двоичная дробь равна 3602879701896397/2 ** 55, что близко, но не точно равно истинному значению 1/10.

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

>>> 0.1
# 0.1000000000000000055511151231257827021181583404541015625

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

Просто помните, что хотя напечатанный результат выглядит как точное значение 1/10, фактическое сохраненное значение является ближайшей представимой двоичной дробью.

Интересно, что существует много разных десятичных чисел, которые имеют одну и ту же ближайшую приблизительную двоичную дробь. Например, числа 0.1 и 0.10000000000000001 и 0.1000000000000000055511151231... все приблизительно равны 3602879701896397/2 ** 55. Поскольку все эти десятичные значения имеют одинаковую аппроксимацию, любое из них может отображаться при сохранении инварианта eval(repr(x)) == x.

Исторически Python и встроенная функция repr() выбирали функцию с 17 значащими цифрами, 0.10000000000000001. Начиная с Python 3.1 в большинстве систем теперь может выбирать самый короткий из них и просто отображать 0.1.

Обратите внимание, что это по своей природе двоичное число с плавающей точкой: это не ошибка в Python и не ошибка в вашем коде. Вы увидите то же самое на всех языках, которые поддерживают арифметику с плавающей запятой.

Для более приятного вывода вы можете использовать форматирование строки для получения ограниченного числа значащих цифр:

>>> import math
>>> format(math.pi, '.12g')
# '3.14159265359'
>>> format(math.pi, '.2f')
# '3.14'
>>> repr(math.pi)
# '3.141592653589793'

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

Одна иллюзия может породить другую. Например, поскольку 0,1 не является точно 1/10, суммирование трех значений 0,1 может также не дать точно 0,3:

>>> 0.1 + 0.1 + 0.1 == 0.3
# False

Кроме того, поскольку 0,1 не может приблизиться к точному значению 1/10, а 0,3 не может приблизиться к точному значению 3/10, предварительное округление функцией round() может не помочь:

>>> round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1)
# False

Двоичная арифметика с плавающей точкой содержит много сюрпризов, подобных этому. Проблема с 0.1 подробно объясняется в разделе «Ошибка представления». Смотрите также «Опасности с плавающей точкой» для более полного описания других распространенных сюрпризов.

Как говорится, «простых ответов нет». Тем не менее, не следует чрезмерно опасаться чисел с плавающей запятой! Ошибки в операциях с плавающей запятой Python наследуются от аппаратного обеспечения чисел с плавающей запятой, и на большинстве машин они имеют порядок не более одной части в 2 ** 53 на операцию. Это более чем достаточно для большинства задач, но вам нужно помнить, что это не десятичная арифметика и что каждая операция с плавающей запятой может подвергаться новой ошибке округления.

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

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

Другая форма точной арифметики поддерживается модулем fractions, который реализует арифметику, основанную на рациональных числах, поэтому числа, такие как 1/3 могут быть представлены точно.

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

Python предоставляет инструменты, которые могут помочь в тех редких случаях, когда вы действительно хотите узнать точное значение числа с плавающей точкой. Метод float.as_integer_ratio() выражает значение типа float в виде дроби:

>>> x = 3.14159
>>> x.as_integer_ratio()
# (3537115888337719, 1125899906842624)

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

>>> x == 3537115888337719 / 1125899906842624
# True

Метод float.hex() выражает число с плавающей запятой в шестнадцатеричном формате (основание 16), снова давая точное значение, сохраненное компьютером:

>>> x.hex()
# '0x1.921f9f01b866ep+1'

Это точное шестнадцатеричное представление может быть использовано для точного восстановления значения с плавающей точкой:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True

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

Другим полезным инструментом является функция math.fsum(), которая помогает уменьшить потерю точности во время суммирования. Она отслеживает «потерянные цифры», когда значения добавляются в промежуточный итог. Это может повлиять на общую точность, так что ошибки не накапливаются до такой степени, что бы влиять на итоговую сумму:

>>> import math
>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True

Ошибка представления.

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

Ошибка представления относится к тому факту, что фактически большинство десятичных дробей не могут быть представлены точно как двоичные дроби (основание 2) . Это главная причина, почему Python или Perl, C, C++, Java, Fortran и многие другие языки часто не будут отображать точное десятичное число, которое ожидаете.

Это почему? 1/10 не совсем представимо в виде двоичной дроби. Почти все машины сегодня используют арифметику IEEE-754 с плавающей точкой, и почти все платформы отображают плавающие значения Python в IEEE-754 с «двойной точностью». При вводе, компьютер стремится преобразовать 0,1 в ближайшую дробь по форме J / 2 ** N, где J — целое число, содержащее ровно 53 бита.

Перепишем 1 / 10 ~= J / (2**N) как J ~= 2**N / 10 и напоминая, что J имеет ровно 53 бита, это >= 2**52, но < 2**53, наилучшее значение для N равно 56:

>>> 2**52 <=  2**56 // 10  < 2**53
# True

То есть 56 — единственное значение для N, которое оставляет J точно с 53 битами. Наилучшее возможное значение для J тогда будет округлено:

>>> q, r = divmod(2**56, 10)
>>> r
# 6

Поскольку остаток больше половины от 10, наилучшее приближение получается округлением вверх:

>>> q+1
# 7205759403792794

Поэтому наилучшее возможное приближение к 1/10 при двойной точности 754: 7205759403792794 / 2 ** 56. Деление числителя и знаменателя на два уменьшает дробь до: 3602879701896397 / 2 ** 55

Обратите внимание, поскольку мы округлили вверх, то значение на самом деле немного больше, чем 1/10. Если бы мы не округлили, то значение был бы немного меньше 1/10. Но ни в коем случае это не может быть ровно 1/10!

Таким образом, компьютер никогда не «видит» 1/10: то, что он видит, это точная дробь, указанная выше, наилучшее двоичное приближение 754, которое он может получить:

>>> 0.1 * 2 ** 55
# 3602879701896397.0

Если мы умножим эту дробь на 10 ** 55, мы увидим значение до 55 десятичных цифр:

>>> 3602879701896397 * 10 ** 55 // 2 ** 55
# 1000000000000000055511151231257827021181583404541015625

Это означает, что точное число, хранящееся в компьютере, равно десятичному значению полученному выше. Вместо отображения полного десятичного значения многие языки, включая более старые версии Python округляют результат до 17 значащих цифр:

>>> format(0.1, '.17f')
'0.10000000000000001'

Модули fractions and decimal упрощают эти вычисления:

>>> from decimal import Decimal
>>> from fractions import Fraction
>>> Fraction.from_float(0.1)
# Fraction(3602879701896397, 36028797018963968)
>>> (0.1).as_integer_ratio()
# (3602879701896397, 36028797018963968)
>>> Decimal.from_float(0.1)
# Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> format(Decimal.from_float(0.1), '.17')
# '0.10000000000000001'

Здесь мы рассмотрим новый пример — перевод значения температуры из шкалы Фаренгейта в шкалу Цельсия. Мы узнаем о старшинстве арифметических операций, об использовании встроенных математических функции, например e(x) или sin(x), о том как можно использовать интерактивную оболочку Python в качестве калькулятора. Кроме того, мы поговорим об ошибках — ошибке целочисленного деления и погрешности вычислений. Для тех, кто знаком с комплексной переменной, я расскажу о том как с ней работать в Python. После этого урока в своих вычислениях вы сможете обходиться без инженерного калькулятора.

Другая формула: Цельсий/Фаренгейт[править]

Следующим нашим примером послужит формула для перехода от градусов Цельсия в соответствующее значение по Фаренгейту:

{displaystyle ~F={frac {9}{5}}C+32} (2.1)

В этой формуле символу С соответствует температура в градусах шкалы Цельсия, а символу F — температура в градусах шкалы Фаренгейта. Наша цель состоит в том, чтобы написать программу, которая расчитывает F, когда мы знаем С.

Ошибка целочисленного деления[править]

Если написать решение, основываясь только на тех знаниях, что мы получили из предыдущего урока, то получим для температуры, например, 21 градус по Цельсию, следующий текст:

C = 21
F = (9 / 5) * C + 32
print (F)

Когда вы запустите программу, программа напишет результат: 53. Вроде бы все работает. Но не стоит забывать о проверке результатов. В этом случае легко проверить результат, в столбик или на калькуляторе:

{displaystyle ~{frac {9}{5}}*21+32=69.8}

Но что мы сделали не так? Ведь формула в программе выглядит правильно. Ошибка в нашей программе одна из самых часто распространенных ошибок, которые по незнанию делают новички. В многих языках программирования существует два типа деления (division): «float division» и «integer division». Float division именно то, чего мы ждем математически: 9/5 равно 1.8, то есть число с плавающей (float) запятой.

Integer division для деления целых чисел в результате дает целое число, отрезая часть после точки, то есть «округляя вниз», до ближайшего меньшего целого, в нашем случае до единицы. То есть целочисленное деление рассматривает сколько целых делителей может поместиться в делимом. Это означает что для целочисленного деления 9/5 = 1. Другие примеры: 16/6 = 2, 1/5 = 0, но заметьте: 18/3 = 6. Многие языки программирования, включая Fortran, C, C++, Java и в том числе Python интерпретируют операцию деления a/b как integer division (целочисленное деление) когда оба числа a и b целые. Если хотя бы один из них является числом с плавающей точкой, то применяется обычное математическое деление.

Проблема нашей программы заключалась именно в целочисленном делении, поскольку часть формулы содержит выражение 9/5. Несложно проследить как получилось число 53. 9/5 в целочисленном делении дает 1, 1 умножается на 21 и в итоге 21 + 32 = 53. Очень скоро мы узнаем как двумя символами можно решить эту проблему и написать корректно работающую программу, но сначала будет полезно познакомиться с часто встречающемся в программировании на Python слове объект (object).

Замечание: В версии Python 3.3 ситуация с делением обстоит так: Integer division отвечает символ «//», float division — «/»

Объекты в Python[править]

Когда мы пишем:

Python создает int (integer, целочисленный) объект и присваивает ему значение 21. Переменная C действует, как имя этого int object. Также, если бы мы написали C = 21.0, Python бы определил это число как float, создал бы соответствующий float объект и присвоил ему значение 21. Таким образом, рассматривая любое выражение присваивания (assignment statement), с левой стороны от знака равенства мы видим имя переменной, а справа от знака равенства видим объект. Можно сказать, что программирование на Python заключается в определении и изменении объектов.

Самое важное, что стоит сейчас понять, что для математики безразлично, напишите вы 21 или 21.0, но в программе Python это две разные вещи: 21 — это объект целого (int) типа, 21.0 — объект типа float. Эти объекты расположены где-то в разных частях компьютерной памяти, и, когда программа встречает переменную, например С, она вызывает нужный объект из нужной области памяти. Причина наличия двух таких типов в том, что числа типа int занимают в памяти меньше места, значит, если в решении задачи не требуется дробных чисел, то работать с целыми числами (числами типа int) программа будет быстрее, выгоднее.

Существует множество типов объектов в Python, и позже мы научимся создавать собственные объекты. Некоторые объекты могут содержать множество данных, не только целые и дробные числа. Например, уже в первом уроке, мы встретились с str (string) объектами, которые выводили строки текста.

Как избежать целочисленного деления[править]

Существует несколько способов сделать это. Наиболее широко используемый метод уже упоминался. Нам просто нужно, чтобы хотя бы один операнд (или делимое, или делитель, или оба вместе) был объектом не типа int, а типа float. Тогда float division просто гарантировано. Как это сделать:

F = (9.0 / 5) * C + 32       # так
F = (9 / 5.0) * C + 32       # или так

Теперь мы знаем почему неправильно работала первая версия нашей программы и как избежать ошибки. Кстати, мы могли получить ту же ошибку еще на первом уроке, если бы вместо 0.5*g*t**2 написали (1/2)*g*t**2 и долго бы думали почему у нас мячик не хочет подпрыгивать и всегда остается в руке (в y = 0). Корректно работающая версия нашей программы теперь выглядит так:

C = 21
F = (9.0 / 5) * C + 32
print (F)

Старшинство арифметических операций[править]

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

Пусть имеется некоторая переменная a. Проиллюстрируем ее карьерный рост в двух примерах

1. 5/9+2*a**4/2: сначала считается 5/9, целочисленное деление дает 0, затем a возводится в четвертую степень и умножается на 2, а затем делится на два. В итоге получаем a4.

2. 5/(9+2)*a**(4/2): сначала считается первая скобка получается 11, 5/11 дает 0. Потом считается вторая скобка, получается 2, и a возводится в квадрат. Потом квадрат a2 умножается на 0 и получается 0.

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

Стандартные математические функции[править]

В математических формулах встречаются такие функции как sin, cos, tan, sinh, cosh, exp, log и так далее. В инженерном калькуляторе есть специальные кнопки для таких функций. Также и в написании программы на Python вы можете использовать такие встроенные функции.

Пример: извлечение квадратного корня[править]

Вспомним наш первый урок и пример о вертикальном движении мяча. Мы определяли координату y в определенный момент времени. Но представим, нам теперь интересно узнать обратное — через сколько времени окажется мячик в определенной координате yc.

{displaystyle ~y_{c}=v_{0}t-{frac {gt^{2}}{2}}}

{displaystyle ~{frac {gt^{2}}{2}}-v_{0}t+y_{c}=0}

{displaystyle ~t_{1}={frac {v_{0}-{sqrt {v_{0}^{2}-2gy_{c}}}}{g}},t_{2}={frac {v_{0}+{sqrt {v_{0}^{2}-2gy_{c}}}}{g}}}

Два значения времени физически объясняются тем, что мяч оказывается в одной и той же точке два раза: пока он летит наверх (t = t1), и обратно — вниз (t = t2 > t1). Чтобы записать полученные выражения для t1 и t2 в программе, у нас должна быть некая функция, извлекающая квадратный корень, также как на калькуляторе для этого есть специальная кнопка. В Python, функция, извлекающая квадратный корень и множество других функции доступны в модуле, который называется math. Чтобы ими пользоваться мы просто должны импортировать этот модуль в программу, написав перед тем местом, где мы будем использовать функции, import math. После этого, чтобы взять квадратный корень от переменной a мы пишем math.sqrt(a). Вот как, например, будет выглядеть наша программа:

# импортируем модуль для работы с математическими функциями
# для быстрых вычислений следует использовать модуль cmath
# в отличие от math - cmath является не скриптом, а исполняемым файлом
import math

v0 = 5
g = 9.81
yc = 0.2

# используем функцию извлечения корня квадратного - math.sqrt
t1 = (v0 - math.sqrt(v0**2 - 2*g*yc))/g
t2 = (v0 + math.sqrt(v0**2 - 2*g*yc))/g
print('At t=%g s and %g s, the height is %g m.' % (t1, t2, yc))

Результат:

At t=0.0417064 s and 0.977662 s, the height is 0.2 m.

import и from … import[править]

Импортировать функции из каких-либо модулей можно двумя способами. Первый мы только что использовали:

Чтобы вызвать функцию нужно записать имя_модуля.имя_функции(), например для присваивания переменной ‘x’ квадратного корня из переменной ‘y’ записывают:

Люди, часто работающие с математическими функциями, найдут запись math.sqrt(y) менее удобной, чем sqrt(y). К счастью, есть альтернативный способ, позволяющий использовать функцию без префикса имени модуля. Этот альтернативный способ имеет вид «from module import function». Например:

Если в вычислениях вам требуется несколько функций, вы можете перечислить их через запятую:

from math import sqrt, exp, log, sin

Иногда даже пишут:

и тогда из модуля импортируются все функции и константы. Это удобный способ, если вам требуется импортировать много функций (что встречается, кстати, не так часто). Но вы можете столкнуться с ошибками, поскольку вы импортируете множество имен, за которыми вы не следите, и имена ваших функций и переменных могут совпасть, а это приведет к некорректным результатам. Поэтому рекомендуется импортировать только те функции и константы, которые вам действительно нужны. Обычно инструкция import находится в начальной части программы, и, по мере написания программы, вы добавляете к этой инструкции новые модули и функции, которые вам требуются.

import as[править]

Еще одним удобным вариантом является импорт с новым именем, когда одновременно с добавлением модуля или функции мы называем их как нам удобно, то есть,

import math as m
# m теперь имя math в программе
v = m.sin(m.pi)

from math import log as ln
v = ln(5)

from math import sin as s, cos as c, log as ln
v = s(x)*c(x) + ln(x)

Как мы помним, в Python все объекты и переменные могут ссылаться на объекты модулей и функций также как на объекты строк и чисел, например:

m = math
ln = m.log
s = m.sin
c = m.cos

Пример: другие математические функции[править]

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

{displaystyle ~{text{sinh}}(x)={frac {1}{2}}(e^{x}-e^{-x})} (2.2)

Для того, чтобы вызвать гиперболический синус, мы можем использовать три способа: 1) самый простой — вызвать встроенную функцию math.sinh( ), 2)использовать два раза функцию math.exp( ), 3) использовать возведение в степень числа e. Сравним в небольшой программе, как это будет выглядеть и получаемые результаты:

from math import sinh, exp, e, pi
x = 2*pi
r1 = sinh(x)
r2 = 0.5*(exp(x) - exp(-x))
r3 = 0.5*(e**x - e**(-x))
print (r1, r2, r3)

На выходе имеем идентичные результаты:

267.744894041 267.744894041 267.744894041
Чисто технически расчёт для r1 по времени самый короткий, и именно его стоит использовать в программах.
Кроме всего прочего, не стоит забывать, что обращение к локально объявленным функциям происходит быстрее, чем к функции из другого модуля(s=math.sinh вместо math.sinh).

dir( ) и help( )[править]

Для того, чтобы посмотреть все, что вы можете извлечь из какого-либо модуля, достаточно после того как вы его импортировали в интерактивном режиме, написать dir(имя_модуля). Например, это может выглядеть так:

>>> import math
>>> dir(math)
['__doc__', '__name__', '__package__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh',
'ceil','copysign', 'cos', 'cosh', 'degrees', 'e', 'exp', 'fabs', 'factorial', 'floor', 'fmod',
'frexp', 'fsum', 'hypot', 'isinf', 'isnan', 'ldexp', 'log', 'log10', 'log1p', 'modf', 'pi', 'pow',
'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc']

Имена с двумя подчеркиваниями относятся к системным, и о них мы поговорим несколькими уроками позже. Все остальные — это функции и константы, все, что мы непосредственно используем. Для того, чтобы узнать о назначении каждой функции, достаточно написать help(имя_модуля.имя_функции). Аналогично можно просмотреть и всю информацию о модуле, например help(math).

Ошибки округления[править]

Как было показано в предыдущем примере с гиперболическим синусом, мы получили три одинаковых результата, несмотря на форму представления. Попробуем вывести тот же результат с 16 знаками после точки:

print ('%.16f %.16f %.16f' % (r1, r2, r3))

И тогда на выходе:

267.7448940410164369 267.7448940410164369 267.7448940410163232

Теперь r1 и r2 равны, но r3 отличается в последних четырех знаках.

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

print ('%.16f %.16f' % (1 / 49.0 * 49, 1 / 51.0 * 51))

дает нам:

0.9999999999999999 1.0000000000000000

Причина, по которой мы не получаем 1.0 в первом случае заключена в том, что 1/49 не может быть представлена в памяти компьютера корректно. На самом деле и 1/51 неточно записывается в памяти, но эта неточность не видна в конечном результате.

В итоге, ошибки в числах с плавающей точкой могут накапливаться в наших математических вычислениях и результаты будут являться аппроксимацией к абсолютно точным, «природным» ответам. Эти ошибки называются round-off errors, ошибки округления, ошибки точности. В Python есть специальный модуль decimal, позволяющий представлять действительные числа с регулируемой точностью, то есть снижать погрешность до любых требуемых значений. Однако, мы практически не будем пользоваться этим модулем, поскольку большинство используемых математических методов и так приближены, и вносят гораздо большую неточность, чем round-off errors. Более того, в расчётах из области радиоэлектроники, например, три значащих цифры в параметрах какого-либо элемента являются вполне достаточными.

Числа с фиксированной точностью[править]

Начиная с версии Python 2.4 появился новый числовой тип: числа с фиксированной точностью представления. Такие числа создаются с помощью вызова из специального модуля decimal. Числа эти похожи на числа с плавающей запятой, но число знаков после запятой здесь фиксировано, отсюда и такое название. То есть так можно хранить число, которое всегда будет иметь, например, два знака после запятой. Кроме того, можно указать, как должны обрабатываться лишние десятичные знаки — просто отсекаться или округляться. Скорость работы с такими числами, конечно, меньше, но они могут быть востребованы в областях, где сохранение точности может быть существенно важным, например, в вопросах финансов.

Разговор о точности работы с обычными числами дополняет и следующий простой пример:

>>> 0.1 + 0.1 + 0.1 - 0.3
5.5511151231257827e-17
>>> print (0.1 + 0.1 + 0.1 - 0.3)
5.55111512313e-17

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

>>> from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.1') + Decimal('0.1') - Decimal('0.3')
Decimal('0.0')

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

>>> Decimal('0.1') + Decimal('0.10') + Decimal('0.10') - Decimal('0.3')
Decimal('0.00')

Другие инструменты модуля decimal позволяют задать точность представления и многое другое. Например, объект getcontext из этого модуля позволяет задать точность (число знаков после запятой) и режим округления (вверх, вниз и т. д.):

>>> Decimal(1)/Decimal(7)
Decimal('0.1428571428571428571428571429')
>>> from decimal import getcontext
>>> getcontext().prec = 4
>>> Decimal(1)/Decimal(7)
Decimal('0.1429')

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

Интерактивное программирование[править]

Особенно удобная возможность, предоставляемая Python — это возможность быстро проверить как работают инструкции или выражения в интерактивном режиме. Такие интерактивные оболочки называются shell. Мы уже пользовались ей как частью IDLE. Остановимся сейчас на ней поподробнее.

Interactive Shell[править]

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

>>> v0 = 5
>>> g = 9.81
>>> t = 0.6
>>> y = v0 * t - 0.5 * g * t ** 2
>>> print (y)
1.2342

Мы можем легко задать новое значение переменной, например для v0, и всегда способны проконтролировать, что новое значение присвоено, введя имя переменной напрямую или через инструкцию print::

>>> v0 = 6
>>> v0
6
>>> print (v0)
6

Следующий шаг — обновить выражения, в которых еще содержится старое значение, то есть нашу формулу для y. В таком редакторе как IPython можно найти предыдущее выражение, с помощью кнопок со стрелками. О возможностях этого редактора мы еще поговорим. А пока:

>>> y = v0 * t - 0.5 * g * t ** 2
>>> y
1.8341999999999996
>>> print (y)
1.8342

Причина, по которой мы получили сейчас два разных результата, в том, что, если вы просто пишите y, то вы увидите все знаки после точки, находящиеся в памяти компьютера (16), в то время как инструкция print ограничивает число знаков и, таким образом, дает ответ с достаточной точностью. Впрочем, в некоторых версиях Python автоматически округляет значение переменной.

Преобразование типа[править]

Если вы ранее работали с другими языками программирования, то могли заметить, что мы пишем программы не заботясь о задании типов переменных (вернее даже типов объектов, на которые ссылаются переменные). Однако в этом уроке мы встретились с вопросом типов объектов, когда изучали ошибку целочисленного деления, которая научила нас более внимательно относиться к типам объектов. Следующий пример иллюстрирует функцию type( ) и то, как мы можем изменять тип объекта. Для начала, создадим объект типа int, на который будет ссылаться имя С:

>>> C = 21
>>> type(C)
<type 'int'>

Теперь мы преобразуем наш объект С типа int к соответствующему объекту типа float:

>>> C = float(C)     #  type  conversion
>>> type(C)
<type 'float'>
>>> C
21.0

В инструкции C = float(C) мы создали новый объект из оригинального объекта с тем же именем. Теперь имя С ссылается на новый объект. Изначальный объект типа int со значением 21 теперь недоступен, поскольку у него больше нет имени и автоматически удаляется из памяти.

Мы также можем сделать и обратное, то есть конвертировать объект типа float к объекту типа int:

>>> C = 20.9
>>> type(C)
<type 'float'>
>>> D = int(C) #  type  conversion
>>> type(D)
<type 'int'>
>>> D
20  # decimals  are  truncated  :-/

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

>>> round(20.9)
21.0
>>> int(round(20.9))
21

Комплексные числа[править]

Пусть x2 = 2. Большинство из нас будут способны найти первый корень {displaystyle x={sqrt {2}}}. Более продвинутые в математике заметят, что имеется и второй корень {displaystyle x=-{sqrt {2}}}. Но встретившись с выражением x2 = -2 решение смогут найти лишь немногие люди, знакомые с комплексными числами. Если вы только собираетесь стать ученым, настоятельно советуем познакомиться сейчас с этими замечательными числами перед тем, как изучить дальнейшие примеры, на которых вы сможете проверить свои новые знания.

Комплексная арифметика в Python[править]

Python поддерживает расчеты с комплексными числами. Мнимая часть записывается через j, вместо i в математике. Комплексное число 2-3i соответственно будет записано как 2-3j. Ниже показан простой сеанс работы с комплексными числами и примеры простой комплексной арифметики:

>>> u = 2.5 + 3j       # создаем комплексное
>>> v = 2              # а это целое
>>> w = u + v          # комплексное + целое
>>> w
(4.5+3j)

>>> a = -2
>>> b = 0.5
>>> s = a + b*1j       # complex из двух float
>>> s = complex(a, b)  # другой способ
>>> s
(-2+0.5j)
>>> s*w                # complex*complex
(-10.5-3.75j)
>>> s/w                # complex/complex
(-0.25641025641025639+ 0.28205128205128205j)

Кроме того объект типа complex легко может быть разложен на реальную и мнимую части и для него может быть найдено сопряженное (conjugate) число:

>>> s.real
-2.0
>>> s.imag
0.5
>>> s.conjugate()
(-2-0.5j)

Комплексные функции Python[править]

Для работы с функциями комплексных переменных следует использовать специальную библиотеку — cmath:

>>> from cmath import sin, sinh
>>> r1 = sin(8j)
>>> r1
1490.4788257895502j
>>> r2 = 1j*sinh(8)
>>> r2
1490.4788257895502j

Чему мы научились[править]

Теперь мы знаем об ошибке целочисленного деления и как ее избежать, как можно импортировать (import) модули, узнать что в них имеется (dir) и как использовать (help) их функции. Мы познакомились с концепцией объектов в Python и умеем изменять тип объекта (int, float, string). Кроме привычной математики действительных чисел библиотеки math, мы можем пользоваться и комплексной переменной с помощью модуля cmath. Наконец, теперь в случае, когда мы хотим быстро себя проверить, мы можем воспользоваться интерактивной оболочкой IDLE.

Упражнения[править]

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

Объясните, из-за чего происходит вот такая штука:

>>> 1 - 0.7
0.30000000000000004

и как с этим бороться?

Kromster's user avatar

Kromster

13.5k12 золотых знаков43 серебряных знака72 бронзовых знака

задан 25 янв 2014 в 19:04

Montreal's user avatar

4

Дело в том, что в памяти числа хранятся в двоичном виде, а 0.3 в двоичном виде выглядит как бесконечная периодическая дробь (0.0100110011001…). В память бесконечное число цифр, понятное дело, не запишешь, поэтому реально там хранится число чуть меньше, чем 0.3. При вычитании этого числа из 1 и выводе на экран эта небольшая разница и выплывает наружу. Что с этим делать — смириться с несовершенством мира и при выводе округлять до приемлемого числа знаков.

Подробнее о представлении чисел с плавающей запятой тонкостях операций с ними: статья на Хабре

ответ дан 25 янв 2014 в 20:04

insolor's user avatar

insolorinsolor

45.9k16 золотых знаков54 серебряных знака95 бронзовых знаков

Такова природа чисел с плавающей запятой: они описываются через экспоненту, и их «шаг» может выглядеть довольно странно, однако обычно он достаточно мал, чтобы избежать серьезных ошибок. В документации, насколько понял, рекомендуют использовать round().

ответ дан 25 янв 2014 в 19:25

etki's user avatar

etkietki

36.1k2 золотых знака55 серебряных знаков81 бронзовый знак

6

Ещё можно попытаться привести числа с плавающей запятой к целым числам. Например, следующее неравенство:

1 - 0.7 > 0.3

(на которое python мне ответил True) можно заменить на эквивалентное домножив обе части на 10:

10 - 7 > 3

Тут я уже, как и ожидалось, получил False.

ответ дан 24 фев 2016 в 20:57

pank's user avatar

pankpank

2,5481 золотой знак14 серебряных знаков30 бронзовых знаков

2

Содержание:развернуть

  • Как устроен механизм исключений
  • Как обрабатывать исключения в Python (try except)
  • As — сохраняет ошибку в переменную

  • Finally — выполняется всегда

  • Else — выполняется когда исключение не было вызвано

  • Несколько блоков except

  • Несколько типов исключений в одном блоке except

  • Raise — самостоятельный вызов исключений

  • Как пропустить ошибку

  • Исключения в lambda функциях
  • 20 типов встроенных исключений в Python
  • Как создать свой тип Exception

Программа, написанная на языке Python, останавливается сразу как обнаружит ошибку. Ошибки могут быть (как минимум) двух типов:

  • Синтаксические ошибки — возникают, когда написанное выражение не соответствует правилам языка (например, написана лишняя скобка);
  • Исключения — возникают во время выполнения программы (например, при делении на ноль).

Синтаксические ошибки исправить просто (если вы используете IDE, он их подсветит). А вот с исключениями всё немного сложнее — не всегда при написании программы можно сказать возникнет или нет в данном месте исключение. Чтобы приложение продолжило работу при возникновении проблем, такие ошибки нужно перехватывать и обрабатывать с помощью блока try/except.

Как устроен механизм исключений

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

💁‍♂️ Пример: напишем скрипт, в котором функция ожидает число, а мы передаём сроку (это вызовет исключение «TypeError»):

def b(value):
print("-> b")
print(value + 1) # ошибка тут

def a(value):
print("-> a")
b(value)

a("10")

> -> a
> -> b
> Traceback (most recent call last):
> File "test.py", line 11, in <module>
> a("10")
> File "test.py", line 8, in a
> b(value)
> File "test.py", line 3, in b
> print(value + 1)
> TypeError: can only concatenate str (not "int") to str

В данном примере мы запускаем файл «test.py» (через консоль). Вызывается функция «a«, внутри которой вызывается функция «b«. Все работает хорошо до сточки print(value + 1). Тут интерпретатор понимает, что нельзя конкатенировать строку с числом, останавливает выполнение программы и вызывает исключение «TypeError».

Далее ошибка передается по цепочке в обратном направлении: «b» → «a» → «test.py«. Так как в данном примере мы не позаботились обработать эту ошибку, вся информация по ошибке отобразится в консоли в виде Traceback.

Traceback (трассировка) — это отчёт, содержащий вызовы функций, выполненные в определенный момент. Трассировка помогает узнать, что пошло не так и в каком месте это произошло.

Traceback лучше читать снизу вверх ↑

Пример Traceback в Python

В нашем примере Traceback содержится следующую информацию (читаем снизу вверх):

  1. TypeError — тип ошибки (означает, что операция не может быть выполнена с переменной этого типа);
  2. can only concatenate str (not "int") to str — подробное описание ошибки (конкатенировать можно только строку со строкой);
  3. Стек вызова функций (1-я линия — место, 2-я линия — код). В нашем примере видно, что в файле «test.py» на 11-й линии был вызов функции «a» со строковым аргументом «10». Далее был вызов функции «b». print(value + 1) это последнее, что было выполнено — тут и произошла ошибка.
  4. most recent call last — означает, что самый последний вызов будет отображаться последним в стеке (в нашем примере последним выполнился print(value + 1)).

В Python ошибку можно перехватить, обработать, и продолжить выполнение программы — для этого используется конструкция try ... except ....

Как обрабатывать исключения в Python (try except)

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

Например, вот как можно обработать ошибку деления на ноль:

try:
a = 7 / 0
except:
print('Ошибка! Деление на 0')

Здесь в блоке try находится код a = 7 / 0 — при попытке его выполнить возникнет исключение и выполнится код в блоке except (то есть будет выведено сообщение «Ошибка! Деление на 0»). После этого программа продолжит свое выполнение.

💭 PEP 8 рекомендует, по возможности, указывать конкретный тип исключения после ключевого слова except (чтобы перехватывать и обрабатывать конкретные исключения):

try:
a = 7 / 0
except ZeroDivisionError:
print('Ошибка! Деление на 0')

Однако если вы хотите перехватывать все исключения, которые сигнализируют об ошибках программы, используйте тип исключения Exception:

try:
a = 7 / 0
except Exception:
print('Любая ошибка!')

As — сохраняет ошибку в переменную

Перехваченная ошибка представляет собой объект класса, унаследованного от «BaseException». С помощью ключевого слова as можно записать этот объект в переменную, чтобы обратиться к нему внутри блока except:

try:
file = open('ok123.txt', 'r')
except FileNotFoundError as e:
print(e)

> [Errno 2] No such file or directory: 'ok123.txt'

В примере выше мы обращаемся к объекту класса «FileNotFoundError» (при выводе на экран через print отобразится строка с полным описанием ошибки).

У каждого объекта есть поля, к которым можно обращаться (например если нужно логировать ошибку в собственном формате):

import datetime

now = datetime.datetime.now().strftime("%d-%m-%Y %H:%M:%S")

try:
file = open('ok123.txt', 'r')
except FileNotFoundError as e:
print(f"{now} [FileNotFoundError]: {e.strerror}, filename: {e.filename}")

> 20-11-2021 18:42:01 [FileNotFoundError]: No such file or directory, filename: ok123.txt

Finally — выполняется всегда

При обработке исключений можно после блока try использовать блок finally. Он похож на блок except, но команды, написанные внутри него, выполняются обязательно. Если в блоке try не возникнет исключения, то блок finally выполнится так же, как и при наличии ошибки, и программа возобновит свою работу.

Обычно try/except используется для перехвата исключений и восстановления нормальной работы приложения, а try/finally для того, чтобы гарантировать выполнение определенных действий (например, для закрытия внешних ресурсов, таких как ранее открытые файлы).

В следующем примере откроем файл и обратимся к несуществующей строке:

file = open('ok.txt', 'r')

try:
lines = file.readlines()
print(lines[5])
finally:
file.close()
if file.closed:
print("файл закрыт!")

> файл закрыт!
> Traceback (most recent call last):
> File "test.py", line 5, in <module>
> print(lines[5])
> IndexError: list index out of range

Даже после исключения «IndexError», сработал код в секции finally, который закрыл файл.

p.s. данный пример создан для демонстрации, в реальном проекте для работы с файлами лучше использовать менеджер контекста with.

Также можно использовать одновременно три блока try/except/finally. В этом случае:

  • в try — код, который может вызвать исключения;
  • в except — код, который должен выполниться при возникновении исключения;
  • в finally — код, который должен выполниться в любом случае.

def sum(a, b):
res = 0

try:
res = a + b
except TypeError:
res = int(a) + int(b)
finally:
print(f"a = {a}, b = {b}, res = {res}")

sum(1, "2")

> a = 1, b = 2, res = 3

Else — выполняется когда исключение не было вызвано

Иногда нужно выполнить определенные действия, когда код внутри блока try не вызвал исключения. Для этого используется блок else.

Допустим нужно вывести результат деления двух чисел и обработать исключения в случае попытки деления на ноль:

b = int(input('b = '))
c = int(input('c = '))
try:
a = b / c
except ZeroDivisionError:
print('Ошибка! Деление на 0')
else:
print(f"a = {a}")

> b = 10
> c = 1
> a = 10.0

В этом случае, если пользователь присвоит переменной «с» ноль, то появится исключение и будет выведено сообщение «‘Ошибка! Деление на 0′», а код внутри блока else выполняться не будет. Если ошибки не будет, то на экране появятся результаты деления.

Несколько блоков except

В программе может возникнуть несколько исключений, например:

  1. Ошибка преобразования введенных значений к типу float («ValueError»);
  2. Деление на ноль («ZeroDivisionError»).

В Python, чтобы по-разному обрабатывать разные типы ошибок, создают несколько блоков except:

try:
b = float(input('b = '))
c = float(input('c = '))
a = b / c
except ZeroDivisionError:
print('Ошибка! Деление на 0')
except ValueError:
print('Число введено неверно')
else:
print(f"a = {a}")

> b = 10
> c = 0
> Ошибка! Деление на 0

> b = 10
> c = питон
> Число введено неверно

Теперь для разных типов ошибок есть свой обработчик.

Несколько типов исключений в одном блоке except

Можно также обрабатывать в одном блоке except сразу несколько исключений. Для этого они записываются в круглых скобках, через запятую сразу после ключевого слова except. Чтобы обработать сообщения «ZeroDivisionError» и «ValueError» в одном блоке записываем их следующим образом:

try:
b = float(input('b = '))
c = float(input('c = '))
a = b / c
except (ZeroDivisionError, ValueError) as er:
print(er)
else:
print('a = ', a)

При этом переменной er присваивается объект того исключения, которое было вызвано. В результате на экран выводятся сведения о конкретной ошибке.

Raise — самостоятельный вызов исключений

Исключения можно генерировать самостоятельно — для этого нужно запустить оператор raise.

min = 100
if min > 10:
raise Exception('min must be less than 10')

> Traceback (most recent call last):
> File "test.py", line 3, in <module>
> raise Exception('min value must be less than 10')
> Exception: min must be less than 10

Перехватываются такие сообщения точно так же, как и остальные:

min = 100

try:
if min > 10:
raise Exception('min must be less than 10')
except Exception:
print('Моя ошибка')

> Моя ошибка

Кроме того, ошибку можно обработать в блоке except и пробросить дальше (вверх по стеку) с помощью raise:

min = 100

try:
if min > 10:
raise Exception('min must be less than 10')
except Exception:
print('Моя ошибка')
raise

> Моя ошибка
> Traceback (most recent call last):
> File "test.py", line 5, in <module>
> raise Exception('min must be less than 10')
> Exception: min must be less than 10

Как пропустить ошибку

Иногда ошибку обрабатывать не нужно. В этом случае ее можно пропустить с помощью pass:

try:
a = 7 / 0
except ZeroDivisionError:
pass

Исключения в lambda функциях

Обрабатывать исключения внутри lambda функций нельзя (так как lambda записывается в виде одного выражения). В этом случае нужно использовать именованную функцию.

20 типов встроенных исключений в Python

Иерархия классов для встроенных исключений в Python выглядит так:

BaseException
SystemExit
KeyboardInterrupt
GeneratorExit
Exception
ArithmeticError
AssertionError
...
...
...
ValueError
Warning

Все исключения в Python наследуются от базового BaseException:

  • SystemExit — системное исключение, вызываемое функцией sys.exit() во время выхода из приложения;
  • KeyboardInterrupt — возникает при завершении программы пользователем (чаще всего при нажатии клавиш Ctrl+C);
  • GeneratorExit — вызывается методом close объекта generator;
  • Exception — исключения, которые можно и нужно обрабатывать (предыдущие были системными и их трогать не рекомендуется).

От Exception наследуются:

1 StopIteration — вызывается функцией next в том случае если в итераторе закончились элементы;

2 ArithmeticError — ошибки, возникающие при вычислении, бывают следующие типы:

  • FloatingPointError — ошибки при выполнении вычислений с плавающей точкой (встречаются редко);
  • OverflowError — результат вычислений большой для текущего представления (не появляется при операциях с целыми числами, но может появиться в некоторых других случаях);
  • ZeroDivisionError — возникает при попытке деления на ноль.

3 AssertionError — выражение, используемое в функции assert неверно;

4 AttributeError — у объекта отсутствует нужный атрибут;

5 BufferError — операция, для выполнения которой требуется буфер, не выполнена;

6 EOFError — ошибка чтения из файла;

7 ImportError — ошибка импортирования модуля;

8 LookupError — неверный индекс, делится на два типа:

  • IndexError — индекс выходит за пределы диапазона элементов;
  • KeyError — индекс отсутствует (для словарей, множеств и подобных объектов);

9 MemoryError — память переполнена;

10 NameError — отсутствует переменная с данным именем;

11 OSError — исключения, генерируемые операционной системой:

  • ChildProcessError — ошибки, связанные с выполнением дочернего процесса;
  • ConnectionError — исключения связанные с подключениями (BrokenPipeError, ConnectionResetError, ConnectionRefusedError, ConnectionAbortedError);
  • FileExistsError — возникает при попытке создания уже существующего файла или директории;
  • FileNotFoundError — генерируется при попытке обращения к несуществующему файлу;
  • InterruptedError — возникает в том случае если системный вызов был прерван внешним сигналом;
  • IsADirectoryError — программа обращается к файлу, а это директория;
  • NotADirectoryError — приложение обращается к директории, а это файл;
  • PermissionError — прав доступа недостаточно для выполнения операции;
  • ProcessLookupError — процесс, к которому обращается приложение не запущен или отсутствует;
  • TimeoutError — время ожидания истекло;

12 ReferenceError — попытка доступа к объекту с помощью слабой ссылки, когда объект не существует;

13 RuntimeError — генерируется в случае, когда исключение не может быть классифицировано или не подпадает под любую другую категорию;

14 NotImplementedError — абстрактные методы класса нуждаются в переопределении;

15 SyntaxError — ошибка синтаксиса;

16 SystemError — сигнализирует о внутренне ошибке;

17 TypeError — операция не может быть выполнена с переменной этого типа;

18 ValueError — возникает когда в функцию передается объект правильного типа, но имеющий некорректное значение;

19 UnicodeError — исключение связанное с кодирование текста в unicode, бывает трех видов:

  • UnicodeEncodeError — ошибка кодирования;
  • UnicodeDecodeError — ошибка декодирования;
  • UnicodeTranslateError — ошибка перевода unicode.

20 Warning — предупреждение, некритическая ошибка.

💭 Посмотреть всю цепочку наследования конкретного типа исключения можно с помощью модуля inspect:

import inspect

print(inspect.getmro(TimeoutError))

> (<class 'TimeoutError'>, <class 'OSError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>)

📄 Подробное описание всех классов встроенных исключений в Python смотрите в официальной документации.

Как создать свой тип Exception

В Python можно создавать свои исключения. При этом есть одно обязательное условие: они должны быть потомками класса Exception:

class MyError(Exception):
def __init__(self, text):
self.txt = text

try:
raise MyError('Моя ошибка')
except MyError as er:
print(er)

> Моя ошибка


С помощью try/except контролируются и обрабатываются ошибки в приложении. Это особенно актуально для критически важных частей программы, где любые «падения» недопустимы (или могут привести к негативным последствиям). Например, если программа работает как «демон», падение приведет к полной остановке её работы. Или, например, при временном сбое соединения с базой данных, программа также прервёт своё выполнение (хотя можно было отловить ошибку и попробовать соединиться в БД заново).

Вместе с try/except можно использовать дополнительные блоки. Если использовать все блоки описанные в статье, то код будет выглядеть так:

try:
# попробуем что-то сделать
except (ZeroDivisionError, ValueError) as e:
# обрабатываем исключения типа ZeroDivisionError или ValueError
except Exception as e:
# исключение не ZeroDivisionError и не ValueError
# поэтому обрабатываем исключение общего типа (унаследованное от Exception)
# сюда не сходят исключения типа GeneratorExit, KeyboardInterrupt, SystemExit
else:
# этот блок выполняется, если нет исключений
# если в этом блоке сделать return, он не будет вызван, пока не выполнился блок finally
finally:
# этот блок выполняется всегда, даже если нет исключений else будет проигнорирован
# если в этом блоке сделать return, то return в блоке

Подробнее о работе с исключениями в Python можно ознакомиться в официальной документации.

Понравилась статья? Поделить с друзьями:
  • Ошибки вытяжной вентиляции
  • Ошибки вытяжки аристон
  • Ошибки выступления перед аудиторией
  • Ошибки выступающего перед аудиторией
  • Ошибки выразительного чтения