Числа

Числа в языке Python представлены тремя встроенными типами: целые (int), вещественные (float) и комплексные (comlex), а так же двумя типами чисел, которые предоставляет его стандартная библиотека: десятичные дроби неограниченной точности (Decimal) и обыкновенные дроби (Float).

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

Начнем с того, что числовые литералы не содержат знаки "+" и "-", т.е. с точки зрения Python число \(-3.14\) не является единым отрицательным числом, а является командой, которая состоит из унарного оператора (-) и числа \(3.14\). Это говорит о том, что знаки "+" и "-" хоть и способны обозначать положительные и отрицательные числа, но на самом деле эти знаки являются операторами:

>>> -+-+--++1
1

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

>>> x = 1
>>> x = x - 1
>>> x
0

Вроде бы мы создали объект-число со значением \(1\), а затем изменили его значение на \(0\). Но если это так, то id объекта (его адрес в памяти) не должен меняться, а он меняется:

>>> x = 1
>>> id(x)
7916480
>>> 
>>> x = x - 1
>>> x
0
>>> 
>>> id(x)
7916464

Как видим, изменения объекта не произошло, старый объект исчез и появился новый. Эту информацию трудно назвать "очень полезной", но она нужна для понимания "внутренней кухни" языка. Например того, что все в этом языке, включая числа, является объектом. Или того, что числа, действительно хранятся в памяти компьютера, и живут в этой памяти по каким-то своим правилам. Но вся эта внутренняя кухня является заботой самого Python. В основном, мы пишем код, даже не задумываясь о ее существовании, лишь иногда, очень редко вмешиваясь в ее работу.

Ну и последнее, что я могу сказать о встроенных числовых типах, так это то, что результат всегда преобразуется к более общему типу если это необходимо. Например, если частное двух целых чисел (int) не является целым числом, то оно будет преобразовано к вещественному типу (float):

>>> 1/25
0.04

А если результат не может быть выражен типами int и float, то он будет преобразован к типу complex. Так что если вдруг будете вычислять корни четной степени из отрицательных чисел, не ждите ошибки:

>>> (-3)**0.5
(1.0605752387249068e-16+1.7320508075688772j)
>>> 
>>> (-3.14)**0.5
(1.0850398284807605e-16+1.772004514666935j)

Причем такое преобразование работает только в одну сторону int -> float -> complex:

>>> 0.25*8    #  результатом будет все равно "float"
2.0
>>> 
>>> 1 + 1j - 1j    # все равно "complex"
(1+0j)

Еще мы могли бы рассмотреть как устроено хеширование чисел, или как устроены их классы. Но такие тонкости настолько редко требуются на практике, что я о них ни разу не слышал (что и не мудрено, я ведь сельский учитель 😇). А к тому моменту, когда вам вдруг это потребуется вы и сами без труда сможете со всем разобраться. Давайте рассмотрим каждый тип чисел в отдельности.


Целые числа (int)

В самом общем смысле, целые числа - это самые обыкновенные целые числа со знаком или без, например: \(-11, 126, 0\) или \(401734511064747568885490523085290650630550748445698208825344\). Последнее число в примере может показаться несовсем правдоподобным, но это \(2^{198}\), в чем очень легко убедиться:

>>> 2**198
401734511064747568885490523085290650630550748445698208825344

Да, длинная арифметика, нам доступна, что называется "из коробки". А это, надо сказать, очень приятный бонус, например, вы можете легко убедиться в том что \(561\) - число Кармайкла, действительно проходит тест Ферма:

>>> 2**(561-1)%561 == 1
True

Однако, если вы попытаетесь проверить это для числа \(9746347772161\), то результата придется ждать очень долго (если вообще дождемся), вероятнее всего компьютер "встанет колом" и его придется перезагружать. Но вот если воспользоваться встроенной функцией pow(), то результат будет получен моментально:

>>> pow(2, 9746347772160, 9746347772161) == 1
True

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

2 ** 9746347772160 % 9746347772161

Поддержка длинной арифметики может показаться излишней, но на самом деле, есть целая куча подразделов математики (например, комбинаторика, теория графов, теория чисел, криптография и т.д.) где ее наличие "под рукой" может сильно облегчить вычисления и даже способствовать вашему самообразованию. Чем, по вашему, изучение отличается от заучивания? Верно, тем что вы сами все проверяете и подвергаете критике.


Вещественные числа (float)

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

В качестве примера возьмем число \(\sqrt{2}\), которое является вещественным потому что мы никогда не сможем выразить его с помощью обыкновенной дроби. А если мы все-таки извлечем корень из двойки, то обнаружим, что это бесконечная десятичная дробь. Но вычислив этот корень на Python:

>>> 2**(1/2)    #  равносильно 2**0.5
1.4142135623730951

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

На самом деле, работая с числами с плавающей точкой, мы очень часто подразумеваем числа вещественные, например вот такое число \(\sqrt[77]{7}\), его мы увидим в виде конечной десятичной дроби:

>>> 7**(1/77)
1.0255935932948266

А число \(7^{-77}\) в виде мантисы \(8.461569363277291\) (опять же конечной десятичной дроби) и порядка \(-66\):

>>> 7**(-77)
8.461569363277291e-66

Кстати, можно было бы подумать, что 8.461569363277291*10**(-66) вернет результат идентичный предыдущему, но:

>>> 8.461569363277291*10**(-66)
8.461569363277292e-66

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

>>> F = len('число с плавающей точкой')
>>> R = len('вещественное число')
>>> F - R
6

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

В общем важно то, что тип float это тип чисел с погрешностью, чисел с ограниченной точностью. И этой точности нам более чем достаточно. Но существуют исключения из правил – ситуации, в которых и пятый и даже миллионный знак после запятой может оказаться очень важен. А бывают даже такие ситуации, когда мы вообще не можем пользоваться десятичными дробями (вещественными числами).


Комплексные числа (complex)

Единственная область где могут пригодиться комплексные числа – это наука, как теоретическая, так и прикладная. Причем, если понаблюдать за теоретиками, то можно заметить, что они, вполне себе, обходятся бумагой и ручкой, а вот прикладники часто, очень часто что-то считают на компьютерах. Причем, комплексные числа нет-нет да появятся, то тут то там. В принципе, раз это надо только ученым, то пусть сами и выкручиваются – они же умные. "Хм... ну так-то я и есть ученый и как бы даже математик" подумал (наверное) создатель языка Гвидо Ван Россум и добавил в Python комплексные числа.

Комплексное число \(z\) – это число вида \(a+bi\), где \(a\) – действительная часть, \(b\) – мнимая часть, а \(i\) – мнимая единица. В Python комплексное число представляется в схожем виде a ± bj, где a и b могут быть любыми числами типа int и float, а j обозначает мнимую единицу, например:

>>> 3 - 7j
(3-7j)

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

$$f(z) = \frac{z}{1+z^{2}}+\frac{z^{2}}{1-z^{3}}$$

при \(z=1+1j\), то знайте – в этом нет ничего сложного:

>>> z = 1 + 1j
>>> 
>>> z/(1 + z**2) + z**2/(1 - z**3)
(0.29230769230769227+0.26153846153846155j)

Десятичные дроби (Decimal)

Числа данного типа позволяют производить вычисления над десятичными дробями с заданной точностью. Возможно, вы сразу задались вопросом: "А разве типом float мы обойтись не можем? Ведь это как бы и десятичные дроби, а погрешности при вычислениях с ними, настолько ничтожны, что мы можем вообще не обращать на них внимания." Чтож, вполне обоснованное замечание, но давайте посмотрим вот на такой пример:

>>> 0.11+0.29
0.39999999999999997

Должно получиться ровно \(0.4\) а получилось \(0.39999999999999997\). Конечно, как вы сказали: на такую погрешность можно вообще не обращать внимания, но как минимум, такой результат сложения кажется странным сам по себе. Ну в самом деле, разве это так трудно правильно сложить? Дело в том, что компьютер использует двоичную арифметику, над числами в двоичном представлении, а конечная десятичная дробь, в двоичном представлении может оказаться бесконечной, бесконечный "хвост" которой и отбрасывается при вычислениях, что в свою очередь и приводит к таким "ничтожным" погрешностям.

Но, как говорится "Дьявол кроется в мелочах" Очень неприятным последствием таких "ничтожно-маленьких" погрешностей является то, что вы не можете точно проверить истинность равенства:

>>> 0.7 + 0.2 - 0.9 == 0
False

Потому что с точки зрения компьютера:

>>> 0.7 + 0.2 - 0.9
-1.1102230246251565e-16

А в финансовой и бухгалтерской среде подобные логические проверки выполняются постоянно.

Вторым неприятным последствием становится то, что погрешности имеют свойство накопления. Расмотрим простой пример:

>>> s = 0
>>> for i in range(100000000):
...     s += 0.1
... 
>>> s
9999999.98112945

Мы \(100000000\) раз сложили число \(0.1\) с самим собой, но вместо \(10000000\) мы получили \(9999999.98112945\), которое отличается от правильного результата на целых \(0.018870549276471138\). В принципе не так уж и сильно, отличается. Да и пример "притянут за уши". Но что-то подобное происходит при решении дифференциальных уравнений. Если с помощью таких уравнений строится траектория космического аппарата, то из-за такой мизерной погрешности он конечно полетит в сторону нужной планеты, но пролетит мимо. А если вы рассчитываете параметры химической реакции, то на компьютере все может выглядеть более чем безобидно, но в действительности, из-за этой мизерной погрешности вполне может произойти взрыв.

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

>>> from decimal import *    #  импортируем модуль
>>> getcontext().prec = 10    #  устанавливаем точность
>>> 
>>> #  Вычислим частное 13/17
... Decimal(13) / Decimal(17)
Decimal('0.7647058824')

Причем точность может быть настолько большой, насколько позволяет мощность компьютера. Допустим, мы хотим видеть результат с точностью \(80\) знаков после запятой (хотя можем увидеть и \(1000\)), вот они:

>>> getcontext().prec = 80    #  меняем точность
>>> 
>>> #  и получаем необходимый результат:
... Decimal(13) / Decimal(17)
Decimal('0.76470588235294117647058823529411764705882352941176470588235294117647058823529412')

Хотелось бы думать, что такая точность доступна абсолютно для всех математических операций и функций, например таких как всякие синусы, косинусы или даже Γ, Β, G, K функции и прочая экзотика. Но нет, слишком хорошо – тоже не хорошо. К тому же все эти и другие функции могут быть получены с помощью базовых математических операций, которые модулем Decimal прекрасно поддерживаются, например:

>>> Decimal(3).sqrt()    #  квадратный корень из 3
Decimal('1.7320508075688772935274463415058723669428052538103806280558069794519330169088000')
>>> 
>>> Decimal(3)**Decimal(1/7)    #  корень 7-й степени
Decimal('1.1699308127586868762703324263880195497962096309602270476311059210484631095336891')
>>> 
>>> Decimal(3).ln()    #  натуральный логарифм
Decimal('1.0986122886681096913952452369225257046474905578227494517346943336374942932186090')

Обыкновенные дроби (Fraction)

Рациональные числа, они же - обыкновенные дроби предоставляются модулем fractions. Обыкновенная дробь в данном модуле представляется в виде пары двух чисел numerator – числитель и denominator – знаменатель:

>>> from fractions import Fraction
>>> 
>>> a = Fraction(21, 49)
>>> a
Fraction(3, 7)

Честно говоря без чисел типа Fraction можно легко обойтись, но из примера видно, что данный модуль выполнил сокращение числителя и знаменателя автоматически, что довольно любопытно и наводит на вопрос "А где бы мне это могло пригодиться?". Самый очевидный ответ – числовые ряды и пределы. Для примера рассмотрим ряд Лейбница, который сходится к \(\pi/4\) (правда медленно... ооочень медленно сходится):

$$\sum_{n = 0}^{\infty}\frac{(-1)^{n}}{2n + 1} = 1-{\frac{1}{3}}+{\frac{1}{5}}-{\frac{1}{7}}+{\frac{1}{9}}-{\frac{1}{11}}+{\frac{1}{13}}-{\frac{1}{15}}+{\frac{1}{17}}-{\frac{1}{19}}+...$$

>>> for n in range(10):
...     print(Fraction((-1)**n, 2*n + 1), end = ', ')
... 
1, -1/3, 1/5, -1/7, 1/9, -1/11, 1/13, -1/15, 1/17, -1/19, 

Или посмотреть на поведение вот такого предела:

$$\pi =\lim \limits _{m\rightarrow \infty }{\frac {(m!)^{4}\,{2}^{4m}}{\left[(2m)!\right]^{2}\,m}}$$

который тоже можно выразить с помощью чисел типа fractions:

>>> for m in range(1, 20):
...     pi = Fraction(factorial(m)**4*2**(4*m), factorial(2*m)**2*m)
...     print(pi, '=', pi.numerator / pi.denominator)
... 
4 = 4.0
32/9 = 3.5555555555555554
256/75 = 3.4133333333333336
4096/1225 = 3.3436734693877552
65536/19845 = 3.3023935500125976
524288/160083 = 3.2751010413348074
4194304/1288287 = 3.255721745232235
134217728/41409225 = 3.2412518708089806
4294967296/1329696225 = 3.2300364664117174
34359738368/10667118605 = 3.221088996975674
274877906944/85530896451 = 3.2137849402931895
4398046511104/1371086188563 = 3.2077097324665482
70368744177664/21972535073125 = 3.202577396894602
562949953421312/176021737014375 = 3.198184286610796
4503599627370496/1409850293610375 = 3.1943814515494275
288230376151711744/90324408810638025 = 3.1910574333896466
18446744073709551616/5786075364399106425 = 3.18812716944714
147573952589676412928/46326420401234675625 = 3.1855246166557545
1180591620717411303424/370882277949065911875 = 3.1831977177392785

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

>>> for m in range(1, 20):
...     pi = factorial(m)**4*2**(4*m), factorial(2*m)**2*m
...     print(pi, '=', pi[0] / pi[1])
... 
(16, 4) = 4.0
(4096, 1152) = 3.5555555555555554
(5308416, 1555200) = 3.4133333333333336
(21743271936, 6502809600) = 3.3436734693877552
(217432719360000, 65840947200000) = 3.3023935500125976
(4508684868648960000, 1376655196815360000) = 3.2751010413348074
(173205637914018447360000, 53200381195863982080000) = 3.255721745232235
(11351204686333112966184960000, 3502105093579160420352000000) = 3.2412518708089806
(1191604063152504866738232360960000, 368913501610175548260089856000000) = 3.2300364664117174
(190656650104400778678117177753600000000, 59190121813899276854174416896000000000) = 3.221088996975674
(44662464226856508810021017591847321600000000, 13897153996490508973748644663944806400000000) = 3.2137849402931895
(14817933731329545066953533132552736971161600000000, 4619474630559975323276844025835605406515200000000) = 3.2077097324665482
(6771440084808050186516157756781419530133543321600000000, 2114372034029222038591505484325171891273728000000000000) = 3.202577396894602
(4162106276767776895443275462152240202713763203881369600000000, 1301396637520996635282929653757676937593407668224000000000000) = 3.198184286610796
(3371306084181899285309053124343314564198148195143909376000000000000, 1055386194578188235620338701709841793640482425798656000000000000000) = 3.1943814515494275
(3535070648527119224992225688911415412468637441871219917848576000000000000, 1107805397526816243090659183103353338471496211051870525849600000000000000) = 3.1910574333896466
(4724042170170136396649210908217125226636689084520418540138094657536000000000000, 1481760895688907823077375232253249480657622975913436911005125836800000000000000) = 3.18812716944714
(7934576813692483813994361028816015004662609173385847306712585996311986176000000000000, 2490822633171928298736325678650274103462162368122535524612428236062720000000000000000) = 3.1855246166557545
(16544671758995490929976945978181310262762174241357072109729454714005989607079936000000000000, 5197500509250676608714096615351420660791267607547096227240108305824465551360000000000000000) = 3.1831977177392785