Особенности представления и работы с действительными числами

Десятичный мир

Для начала представим себе, что данные в компьютерах хранятся в ячейках-"битах", каждое из которых может принимать 10 разных значений. В таком случае очень легко хранить положительные целые числа: каждое число по цифрам записывается в ячейки памяти. Реальный процессор может выполнять арифметические с такими числами, но есть проблема: чем больше цифр в числах, которые он сможет складывать за одну операцию (такт), тем сложнее его проектировать, тем больше тепла он выделяет и энергии потребляет. Поэтому необходимо выбрать некоторое фиксированную "стандартную" длину чисел так, чтобы с одной стороны для большей части основных задач числа туда помещались, с другой стороны были наиболее короткими. Например, можно выбрать длину в 10 цифр для "обычных" чисел и длину 20 для "длинных" (операций с длинными целыми числами за один такт процессора будет выполняться меньше). Кстати, нам потребуется хранить ещё и знак числа. Как лучше всего это сделать — вопрос очень хороший. Для простоты можно считать, что знак мы храним в старшей цифре: если старшая цифра 0, то число положительное, если 1 — то отрицательное.

Целые числа и дополнение до 10

В реальности используется несколько подходов к хранению знака числа, вернее даже к хранению просто целых чисел. Самый "популярный" в данный момент называется "дополнение до двойки" (two’s complement), что для нашего воображаемого десятичного процессора превращается в "дополнение до десятки". Основная идея подхода состоит в следующем. Так как наши числа ограничены 10-ю цифрами, то если в результате арифметической операции возникнет перенос через разряд в 11-ю цифру, то он будет потерян. В таких случаях говорят, что вычисления производятся по модулю $10^{10}$. Пусть у нас есть два числа: отрицательное $x$ и положительное $y$, и нам нужно вычислить $x+y$. Заметим, что по замечанию выше $x+y\equiv (10^{10}+x) + y$ (ведь добавление лишнего $10^{10}$ ничего не меняет, у нас нет "места", чтобы хранить эту цифру). Но число $(10^{10}+x)$ уже заведомо положительное.

Итак, ровно в этом состоит идея: для хранения отрицательного числа $x$ используется положительное число $(10^{10}+x)$. Неотрицательные числа от 0000000000 до 4999999999 хранятся как есть. А числа от 5000000000 до 9999999999 отдаются отрицательным числам, причём $-1$ превращается в $10^{10}-1 = 9999999999$, $-2$ превращается в $10^{10}-2 = 9999999998$, и так далее, $-5000000000$ превращается в... в $-5000000000$. Заметим, что отрицательных чисел "поместилось" на одно больше, чем положительных.

Вот примеры: сложим $8\,512$ и $-3\,628$. $$10^{10}-3628 = 9\,999\,996\,372.$$ Далее $$8\,512 + (-3\,628) \equiv 8\,512 + 9\,999\,996\,372 = 10\,000\,004\,884 \equiv 4\,884.$$
Сложим $-6\,460$ и $-9\,290$. $$(-6\,460) + (-9\,290) \equiv (10^{10}-6\,460) + (10^{10}-9\,290) = 9\,999\,993\,540 + 9\,999\,990\,710 = $$ $$= 19\,999\,984\,250 \equiv 9\,999\,984\,250 \equiv 9\,999\,984\,250 - 10^{10} = (-15\,750).$$

В чём выгода такого подхода? Во-первых, используются все возможные значения (если знак хранить в первой цифре, то будут "потеряны" 80% чисел). Во-вторых, с таким подходом отрицательные числа ничем не отличаются от положительных и не требуется усложнения схем для организации арифметических операций с ними. По модулю $10^{10}$ отлично работают все арифметические операции, поэтому работать будут и вычитание, и умножение.

В реальных чипах используется двоичная система счисления, но в остальном всё устроенно именно так. Один бит — это двоичная цифра. И существуют числа разной длины — в 8, 16, 32 и 64 двоичных цифры. Это зависит от реальных чипов.


Итак, мы научились хранить целые числа. А как хранить действительные числа?

Можно, например, хранить десять цифр целой части и десять цифр дробной. Идея очень простая, но не очень полноценная. Обычное число Авогадро $6.02\cdot10^{23}$ записать уже не получится.

Можно было хранить числитель и знаменатель дроби (рационального числа). Но кроме множества проблем это не решает проблемы с $6.02\cdot10^{23}$ (ведь придётся хранить очень большое число, а, значит, нужно хранить много цифр).

Но если нужно уметь работать с числами вида $6.02\cdot10^{23}$, то может быть прямо так и хранить? Скажем, хранить 15 цифр числа (плюс 1 цифра на знак) и 3 цифры степени (плюс снова 1 цифра на знак степени), в сумме 20 цифр, или "стандартное" длинное число. В этом случае станет возможно работать с числами от $10^{-999}$ до $10^{999}$ с точностью 15 значащих цифр. Значащие цифры в такой записи называются мантиссой, а степень — экспонентой. Число Авогадро при этом можно было бы записать как $602\cdot10^{21}$.

Заметим, что число Авогадро в виде $x\cdot 10^n$ может быть записано по-разному: и как $6.02\cdot10^{23}$, и как $0.602\cdot10^{24}$, и как $602\cdot10^{21}$, и даже парой совсем неудобных способов: $60200000000000\cdot10^{10}$, $0.000000602\cdot10^{30}$. Желательно выбрать из всех таких представлений одно (каноническое). Кажется заманчивым хранить мантиссу в виде обычного целого числа, например хранить число Авогадро в виде $602\cdot10^{21}$. Но это не очень хорошо: в такой записи числа очень неудобно сравнивать: например попробуйте сравнить число Авогадро с числами $6\cdot10^{23}$, $62\cdot10^{22}$, $6021\cdot10^{20}$ или $60199999999999\cdot10^{10}$. Чтобы числа было удобно сравнивать (и ещё по нескольким причинам), используют следующий подход. Положительное число всегда можно домножить на такую степень десятки, чтобы целая часть лежала от 1 до 9. Ровно такое представление и используют для записи, оно называется нормализованным. Итак, каждое ненулевое число может быть представлено в нормализованном виде $\pm \overline{a_0,a_1a_2\ldots a_{13}\ldots}\cdot 10^n$, где $a_0\ne0$. И примерно в этом виде его можно и хранить: знак числа, целая часть, 14 цифр после запятой, знак степени, три цифры степени, в сумме 20 цифр.

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

Кстати, мы ещё пропустили число 0. Его обычно хранят в виде $0\cdot10^0$, тогда для сравнения его с другими числами не потребуется дополнительных ухищрений.

Реальный двоичный мир

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

Действительные числа в питоне

В питоне для хранения действительных чисел используется тип float.

Если вы хотите считать с клавиатуры действительное число, то результат, возращаемый функцией input() необходимо преобразовывать к типу float:

x = float(input())

Действительные (вещественные) числа представляются в виде чисел с десятичной точкой (а не запятой, как принято при записи десятичных дробей в русский текстах). Для записи очень больших или очень маленьких по модулю чисел используется так называемая запись «с плавающей точкой» (также называемая «научная» запись). В этом случае число представляется в виде некоторой десятичной дроби, называемой мантиссой, умноженной на целочисленную степень десяти (порядок или экспонента). Например, расстояние от Земли до Солнца равно 1.496·1011м, а масса молекулы воды 2.99·10-23кг.

Числа с плавающей точкой в программах на языке Питон, а также при вводе и выводе записываются в виде мантиссы, затем пишется буква e, затем пишется порядок. Пробелы внутри этой записи не ставятся. Например, указанные выше константы можно записать в виде 1.496e11 и 2.99e-23. Перед самим числом также может стоять знак минус. При этом запись числа при вводе не обязана быть нормализованной, выражение 17900e-2 также корректно.

Напомним, что результатом операции деления / всегда является действительное число, в то время как результатом операции // является целое число.

Преобразование действительных чисел к целому производится с округлением в сторону нуля, то есть int(1.7) == 1, int(-1.7) == -1.

С действительными числами также работает операция целочисленного деления //, однако если хотя бы одно из чисел (числитель или знаменатель) действительное, то результат операции будет целым действительным числом. Если числа x и y положительны, то x//y — это то, сколько раз число y умещается в x. Например, 0.7//0.2=3.0, 17.9//1=17.0. В общем же случае x//y — это такое целое число $q$ (в формате float), что $0 \leq x-y\cdot q < |y|$.

Сниппеты для ускорения набора кода

Наверняка вы уже столкнулись с тем, что одни и те же куски кода повторяются очень часто. Например, int(input()) или float(input()) или for i in range(...):. Если вы используете PyCharm, то ввод этого кода можно здорово ускорить при помощи сниппетов — live templates. На скриншотах ниже последовательность для настройки. После этого можно будет ввести ii и нажать клавишу tab — и вуаля!

live templates в PyCharm
01 02 03 04 05

В результате ввод простой программы может выглядеть как-то так:
06

Не связывайтесь с действительными числами, если ответ нужен точный!

В задачах, связанных с финансами, всегда требуется точный ответ. Нельзя терять копейки. Когда ответ требуется точный, то связываться с действительными числами (типом float) супер-опасно. Чтобы убедиться в этом, проверьте:

>>> 0.1 + 0.1 + 0.1 == 0.3  # False
>>> 3 * 0.1 == 0.3  # False
>>> int(0.29 * 100) == 29  # False
>>> 0.29 * 100 == 29.0  # False

С точными вычислениями с действительными числами мы ещё встретимся. И разберёмся, почему происходить вот эта вот дичь.

А в следующих трёх задачах нужно избежать использования float'ов. И как можно скорее перейти к целым: в данном случае к число секунд в 12 часах или к копейкам.

Впрочем, можете попробовать сначала не прислушиваться к этому совету :)

Суммы с факториалами в знаменателе

Многие числа, которые математики пишут просто в духе $\sin 17^\circ$, не так просто посчитать на компьютере. Для этого часто нужно посчитать сумму вида $$ \frac{x^0}{0!} + \frac{x^1}{1!} + \frac{x^2}{2!} + \frac{x^3}{3!} + \frac{x^4}{4!} + \ldots $$ Обычно нужно посчитать несколько сотен таких слагаемых. Считать факториалы в знаменателе явно плохая затея:
100!=93 326 215 443 944 152 681 699 238 856 266 700 490 715 968 264 381 621 468 592 963 895 217 599 993 229 915 608 941 463 976 156 518 286 253 697 920 827 223 758 251 185 210 916 864 000 000 000 000 000 000 000 000.
Вместо этого нужно последовательно вычислять следующее слагаемое из предыдущего. Обычно для этого достаточно пары арифметических операций.

Библиотека math

Для проведения вычислений с действительными числами язык Питон содержит много дополнительных функций, собранных в библиотеку (модуль), которая называется math.

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

import math

Функция от одного аргумента вызывается, например, так: math.sqrt(x) (то есть явно указывается, что из модуля math используется функция sqrt (квадратный корень)). Вместо числа x может быть любое число, переменная или выражение. Функция возвращает значение, которое можно вывести на экран, присвоить другой переменной или использовать в выражении:

y = math.sqrt(4)
print(math.sin(math.pi/2))

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

from math import sqrt, sin

y = sqrt(x)
print(sin(pi/2))
Другие способы импортировать функции из модулей

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

>>> print(pow(2, 3, 10))
8
>>> from math import *
>>> print(pow(2, 3, 10))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: pow expected 2 arguments, got 3
>>>
Если необходимо импортировать все функции, но хочется сократить код (имя math ещё достаточно короткое, могло бы быть имя TelegramBotPython, которое уж совсем громоздкое), то можно использовать следующий подход:
import math as mh
y = mh.sqrt(x)
print(mh.sin(mh.pi/2))

Ниже приведен список основных функций модуля math. Более подробное описание этих функций можно найти на сайте с документацией на Питон.

Некоторые из перечисленных функций (int, round, abs) являются стандартными и не требуют подключения модуля math для использования.

ФункцияОписаниеПример
Округление
int(x) Округляет число в сторону нуля. Это стандартная функция, для ее использования не нужно подключать модуль math. int(17.9) == 17;
int(-17.9) == -17
round(x) Округляет число до ближайшего целого. Если дробная часть числа равна 0.5, то число округляется до ближайшего четного числа. round(17.9) == 18;
round(-17.9) == -18
round(x, n) Округляет число x до n знаков после точки. Это стандартная функция, для ее использования не нужно подключать модуль math. round(17.9123, 1) == 17.9;
round(-17.9123, 1) == -17.9
floor(x) Округляет число вниз («пол»), при этом floor(1.5) == 1, floor(-1.5) == -2 floor(17.9) == 17;
floor(-17.9) == -18
ceil(x) Округляет число вверх («потолок»), при этом ceil(1.5) == 2, ceil(-1.5) == -1 ceil(17.9) == 18;
ceil(-17.9) == -17
abs(x) Модуль (абсолютная величина). Это — стандартная функция. abs(17.9) == abs(-17.9) == 17.9
fabs(x) Модуль (абсолютная величина). Эта функция всегда возвращает значение типа float. fabs(-17) == 17.0
Корни, степени, логарифмы
sqrt(x) Квадратный корень. Использование: sqrt(x) sqrt(4) == 2;
sqrt(10) == 3.1622776601683795
pow(a, b) Возведение в степень, возвращает ab. Использование: pow(a,b). Одноимённая функция есть и среди стандартных, но работает она немного по-другому. В частности, у неё есть третий параметр, позволяющий вычислять степени по модулю числа. Например, последние три цифры степени без непосредственного её вычисления можно получить так: pow(179, 10000, 000) pow(3, 2) == 9.0;
pow(4, 3.322) == 100.00996866205664
exp(x) Экспонента, возвращает ex. Использование: exp(x) exp(1) == 2.718281828459045;
exp(2) == 7.38905609893065
log(x) Натуральный логарифм. При вызове в виде log(x, b) возвращает логарифм по основанию b. exp(log(7)) == 6.999999999999999;
log(1024, 2) == 10.0
log2(x) Двоичный логарифм. $\log_2 8 = 3, \log_2 1024 = 10$. pow(2, log2(17)) == 16.999999999999996;
log2(1024) == 10.0
log10(x) Десятичный логарифм. $\log_{10} 10 = 1, \log_{10} 1000 = 3$. log10(100) == 2.0
e Основание натуральных логарифмов \(e\approx2{,}71828...\).
Тригонометрия
sin(x) Синус угла, задаваемого в радианах, $\sin 0 = 0, \sin\frac{\pi}{2}=1$. sin(pi/2) == 1.0
cos(x) Косинус угла, задаваемого в радианах, $\cos 0 = 1, \cos\frac{\pi}{2}=0$. cos(pi/4)**2 == 0.5
tan(x) Тангенс угла, задаваемого в радианах tan(pi/4) == 1.0
asin(x) Арксинус, возвращает значение в радианах
acos(x) Арккосинус, возвращает значение в радианах
atan(x) Арктангенс, возвращает значение в радианах
atan2(y, x) Полярный угол (в радианах) точки с координатами (x, y). atan2(0,1) == 0.0;
atan2(1,0) == pi/2;
atan2(0,-1) == pi;
atan2(-1,0) == -pi/2
hypot(a, b) Длина гипотенузы прямоугольного треугольника с катетами a и b. hypot(3, 4) == 5.0
degrees(x) Преобразует угол, заданный в радианах, в градусы. degrees(pi/3) == 60
radians(x) Преобразует угол, заданный в градусах, в радианы. radians(60) == pi/3
pi Константа 𝜋
Разное
factorial Вычисляет факториал целого числа factorial(6) == 720
gcd Вычисляет НОД (наибольший общий делитель) двух чисел gcd(10, 16) == 2
frexp Представляет действительное числов в виде x == m * 2**e, где либо m = e = 0, либо 0.5<=|m|<1 (в виде мантиссы и экспоненты). frexp(0.1) == (0.8, -3)
.as_integer_ratio Этот метод представляет действительное число в виде несократимой рациональной дроби со знаменателем — степенью двойки. (0.1).as_integer_ratio() ==
(3602879701896397, 36028797018963968)

Число e, экспонента, логарифм, синусы и косинусы

>

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

Число $e$, или число Эйлера, — это замечательная математическая константа, у которого существует множество определений. Например, площадь под графиком гиперболы $1/x$ от 1 до $e$ равна в точности 1. Также если численность человечества каждый год увеличивалась в $e$ раз, то в любой момент времени число живых равнялось бы числу когда-либо живших до этого момента. Экспонентой называется степень числа $e$.

Натуральный логарифм — это функция, обратная к экспоненте. Более понятными являются двоичный и десятичный логарифмы: $\log_2a$ — это такое число, что $2^{\log_2a}=a$. Аналогично определяется десятичный логарифм. Заметим, что десятичный логарифм примерно равен числу цифр в записи целой части числа (может отличаться на 1), а двоичный — числу цифр в двоичной записи (тоже может отличаться в пределах 1).

Функции синус, косинус, тангенс и т.д. называются тригонометрическими. Рассмотрим окружность радиуса 1 с центром в нуле, а также положительное число $x$. Возьмём нитку длины $x$, привяжем её к точке $(1,0)$ и намотаем на окружность против часовой стрелки. Абцисса получившейся точки (координата по оси $Ox$) будет равна $\cos(x)$, а ордината — $\sin(x)$.

Баллистическая траектория

Пусть в момент времени 0 снаряд находится в точке с координатами \(x_0\) и \(y_0\) и имеет начальную скорость, компоненты которой вдоль осей равны \(V_{x0}\) и \(V_{y0}\) соответственно. Ускорение свободного падения равно \(g\). Тогда координаты снаряда в момент времени \(t\) можно найти по формулам: $$x_t=x_0+t\cdot V_{x0}$$ $$y_t=y_0+t\cdot V_{y0}-\displaystyle\frac{g\cdot t^2}{2}$$

Эта кривая — парабола, ветви которой направлены вниз, проходящая через начальную точку $(x_0, y_0)$.

Если компоненты скорости вдоль осей равны \(V_{x}\) и \(V_{y}\), то модуль скорости равен $\sqrt{V_{x}^2+V_{y}^2}$, а угол, под которым летит снаряд равен $\text{atan}(V_{y}/V_{x})$ (арктангенс, пишется как arctg, в питоне atan).

Обратно, если снаряд летит со скоростью $V$ под углом $\alpha$, то компоненты скорости вдоль осей равны $V_{x} = V\cdot\cos(\alpha)$ и $V_{y} = V\cdot\sin(\alpha)$.