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

Но числа в Python — это гораздо больше, чем, собственно, их числовые значения. Поговорим о трёх особенностях чисел в Python, с которыми вы, возможно, не знакомы.
№1: у чисел есть методы
В Python практически всё — это объект. Один из первых объектов, о котором узнаёт тот, кто начинает изучать Python — это str, используемый для представления строк. Возможно, вы сталкивались с использованием методов строк, вроде .lower(), который возвращает новую строку, все символы которой приведены к нижнему регистру:
>>> "HELLO".lower()
'hello'
Числа в Python тоже, как и строки, являются объектами. У них тоже есть методы. Например, целое число можно преобразовать в байтовую строку с помощью метода .to_bytes():
>>> n = 255
>>> n.to_bytes(length=2, byteorder="big")
b'\x00\xff'
Параметр length указывает на количество байтов, которые нужно использовать при составлении байтовой строки, а параметр byteorder определяет порядок байтов. Например, установка параметра byteorder в значение «big» приводит к возврату байтовой строки, в которой старший байт расположен первым, а установка этого параметра в значение «little» приводит к тому, что первым идёт младший байт.
255 — это максимальное значение, которое может принимать 8-битное целое число. Поэтому в нашем случае при вызове метода .to_bytes() можно без проблем воспользоваться параметром length=1:
>>> n.to_bytes(length=1, byteorder="big")
b'\xff'
А вот если записать в n число 256 и вызвать для него .to_bytes() с параметром length=1, будет выдана ошибка OverflowError:
>>> n = 256
>>> n.to_bytes(length=1, byteorder="big")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OverflowError: int too big to convert
Преобразовать байтовую строку в целое число можно, воспользовавшись методом .from_bytes() класса int:
>>> int.from_bytes(b'\x06\xc1', byteorder="big")
1729
Методы класса вызывают, используя имя класса, а не его экземпляр. Именно поэтому в предыдущем примере метод .from_bytes() вызывают, обращаясь к int.
Любопытный факт: 1729 — это самое маленькое положительное число, которое можно представить в виде суммы кубов двух положительных чисел двумя способами. Исторический анекдот связывает это число с индийским математиком Сринивасой Рамануджаном, который рассказал о нём своему наставнику Готфри Харолду Харди.
Харди часто навещал Рамануджана, когда тот, умирая, находился в больнице в Патни. Именно в одно из таких посещений произошёл «инцидент» с номером такси. Харди приехал в Патни на такси, воспользовавшись своим излюбленным транспортным средством. Он вошёл в палату, где лежал Рамануджан. Начинать разговор Харди всегда было мучительно трудно, и он произнёс свою первую фразу: «Если не ошибаюсь, то номер такси, на котором я приехал, 1729. Мне кажется, это скучное число». На что Рамануджан тотчас же ответил: «Нет, Харди! О нет! Это очень интересное число. Это самое малое из чисел, представимых в виде суммы двух кубов двумя различными способами».
Один из способов представления числа 1729 в виде суммы двух кубов — это 13 + 123. Можете отыскать второй способ?
У чисел с плавающей точкой тоже есть методы. Возможно, самый полезный из них — это .is_integer(). Его используют для проверки того, есть ли у числа с плавающей точкой дробная часть:
>>> n = 2.0
>>> n.is_integer()
True
>>> n = 3.14
>>> n.is_integer()
False
Вот — интересный метод .as_integer_ratio(). Он, вызванный для числа с плавающей точкой, возвращает кортеж, содержащий числитель и знаменатель дроби, представляющей это число:
>>> n.as_integer_ratio()
(1, 2)
Правда, из-за ошибки представления чисел с плавающей точкой, иногда этот метод возвращает неожиданные результаты:
>>> n = 0.1
>>> n.as_integer_ratio()
(3602879701896397, 36028797018963968)
Если надо — можно вызывать методы на числовых литералах, заключённых в круглые скобки:
>>> (255).to_bytes(length=1, byteorder="big")
b'\xff'
>>> (3.14).is_integer()
False
Если обойтись без скобок — при попытке вызова метода на целочисленном литерале будет выдана ошибка SyntaxError. А вот при вызове метода числового литерала с плавающей точкой отсутствие скобок, что странно, не приведёт к ошибке:
>>> 255.to_bytes(length=1, byteorder="big")
File "<stdin>", line 1
255.to_bytes(length=1, byteorder="big")
^
SyntaxError: invalid syntax
>>> 3.14.is_integer()
False
Полный список методов числовых Python-типов можно найти в документации.
№2: числа обладают иерархией
В математике числа обладают естественной иерархией. Например, все натуральные числа являются целыми, а все целые числа — рациональными. Все рациональные числа — это вещественные числа, а все вещественные числа — это комплексные числа.
Похожие рассуждения применимы и к представлению чисел в Python. Здесь «числовая башня» выражается через абстрактные типы, содержащиеся в модуле numbers.
Числовая башня
Все числа в Python являются экземплярами класса Number:
>>> from numbers import Number
>>> # Целые числа являются наследниками Number
>>> isinstance(1729, Number)
True
>>> # Числа с плавающей точкой являются наследниками Number
>>> isinstance(3.14, Number)
True
>>> # Комплексные числа являются наследниками Number
>>> isinstance(1j, Number)
True
Если нужно узнать о том, является ли некое Python-значение числовым, но при этом неважно то, каким именно числовым типом оно представлено, воспользуйтесь конструкцией isinstance(value, Number).
В Python имеется четыре дополнительных абстрактных типа, иерархия которых, начиная с наиболее общего числового типа, выглядит так:
Класс
Complexиспользуется для представления комплексных чисел. Тут имеется один встроенный конкретный тип —complex.Класс
Real— это представление вещественных чисел. Его единственный встроенный конкретный тип —float.Класс
Rationalпредставляет рациональные числа. Его единственным встроенным конкретным типом являетсяFraction.Класс
Integralприменяют для представления целых чисел. В нём имеется два встроенных конкретных типа —intиbool.
Так, погодите, а значения типа bool — это разве числа? Да — числа. Можете это проверить, воспользовавшись REPL:
>>> import numbers
>>> # Комплексные числа являются наследниками Complex
>>> isinstance(1j, numbers.Complex)
True
>>> # Комплексные числа не являются наследниками Real
>>> isinstance(1j, numbers.Real)
False
>>> # Числа с плавающей точкой являются наследниками Real
>>> isinstance(3.14, numbers.Real)
True
>>> # Числа с плавающей точкой не являются наследниками Rational
>>> isinstance(3.14, numbers.Rational)
False
>>> # Объекты Fractions - это не наследники Rational
>>> from fractions import Fraction
>>> isinstance(Fraction(1, 2), numbers.Rational)
True
>>> # Объекты Fractions - это не наследники Integral
>>> isinstance(Fraction(1, 2), numbers.Integral)
False
>>> # Целые числа - это наследники Integral
>>> isinstance(1729, numbers.Integral)
True
>>> # Логические значения - это наследники Integral
>>> isinstance(True, numbers.Integral)
True
>>> True == 1
True
>>> False == 0
True
Всё это, на первый взгляд, выглядит вполне нормально. Правда, порядок несколько нарушает то, что значения типа bool являются числами.
Странность Python: так как тип bool относится к классу Integral (на самом деле он — прямой наследник int), со значениями True и False можно вытворять довольно необычные вещи.
Например, True можно использовать в роли индекса для того чтобы получить второй элемент итерируемого объекта. А если поделить число на False — будет выдана ошибка ZeroDivisionError.
Попробуйте выполнить «False»[True] и 1 / False в REPL!
Но если присмотреться к числовым типам поближе, окажется, что в иерархии Python-чисел имеется пара своеобразных моментов.
Числа типа Decimal не укладываются в иерархию
Как уже было сказано, в «числовой башне» Python есть 4 конкретных числовых типа, соответствующих четырём абстрактным типам: complex, float, Fraction и int. Но в Python имеется и пятый числовой тип, представленный классом Decimal. Этот тип используется для точного представления десятичных чисел и для преодоления ограничений арифметических операций с плавающей точкой.
Можно предположить, что числа типа Decimal являются наследниками Real, но это, на самом деле, не так:
>>> from decimal import Decimal
>>> import numbers
>>> isinstance(Decimal("3.14159"), numbers.Real)
False
Единственный класс, наследником которого является класс Decimal — это Number:
>>> isinstance(Decimal("3.14159"), numbers.Complex)
False
>>> isinstance(Decimal("3.14159"), numbers.Rational)
False
>>> isinstance(Decimal("3.14159"), numbers.Integral)
False
>>> isinstance(Decimal("3.14159"), numbers.Number)
True
Логично то, что класс Decimal не является наследником Integral. В некоторой степени смысл есть и в том, что Decimal не является наследником Rational. Но почему Decimal не является наследником Real или Complex?
Ответ кроется в исходном коде CPython:
Объекты Decimal обладают всеми методами, определёнными в классе Real, но эти объекты не должны регистрироваться в виде наследников Real, так как Decimal-числа не взаимодействуют с двоичными числами с плавающей точкой (например, результат операции Decimal('3.14') + 2.71828 не определён). Но ожидается, что числа, классы которых являются наследниками абстрактного класса Real, способны взаимодействовать друг с другом (то есть — R1+R2 должно вычисляться в том случае, если числа R1 и R2 представлены типами, являющимися наследниками Real).
Получается, что объяснение странностей сводится к особенностям реализации.
Числа с плавающей точкой — странные создания
А вот числа с плавающей точкой, с другой стороны, реализуют абстрактный базовый класс Real. Они используются для представления вещественных чисел. Но, из-за того, что компьютерная память не является неограниченным ресурсом, числа с плавающей точкой — это лишь конечные аппроксимации вещественных чисел. Это приводит к возможности написания «ненормальных» образцов кода вроде такого:
>>> 0.1 + 0.1 + 0.1 == 0.3
False
Числа с плавающей точкой хранятся в памяти в виде двоичных дробей. Это приводит к появлению некоторых проблем. Например, у дроби 13 нет конечного десятичного представления (после десятичной точки идёт бесконечное множество троек). А у дроби 110 нет конечного представления в виде двоичной дроби.
Другими словами, в компьютере нельзя совершенно точно представить число 0,1 — если только этот компьютер не обладает бесконечной памятью.
Со строго математической точки зрения все числа с плавающей точкой — это рациональные числа, за исключением float(«inf») и float(«nan»). Но программисты используют их в роли аппроксимаций вещественных чисел и воспринимают их, по большей части, как вещественные числа.
Странность Python: float(«nan») — это особое значение с плавающей точкой, представляющее собой «не число». Такие значения часто обозначают как NaN. Но, так как float — это числовой тип, isinstance(float(«nan»), Number) возвращает True.
Получается, что «не числа» — это числа.
В общем, числа с плавающей точкой — странные создания.
№3: набор числовых типов Python можно расширять
Абстрактный числовой базовый тип Python позволяет программисту создавать собственные абстрактные и конкретные числовые типы.
В качестве примера рассмотрим класс ExtendedInteger, который реализует числа в форме a+bp, где a и b — целые числа, а p — простое число (обратите внимание: класс не обеспечивает то, что число p является простым):
import math
import numbers
class ExtendedInteger(numbers.Real):
def init(self, a, b, p = 2) -> None:
self.a = a
self.b = b
self.p = p
self._val = a + (b * math.sqrt(p))
def repr(self):
return f"{self.class.name}({self.a}, {self.b}, {self.p})"
def str(self):
return f"{self.a} + {self.b}√{self.p}"
def trunc(self):
return int(self._val)
def float(self):
return float(self._val)
def hash(self):
return hash(float(self._val))
def floor(self):
return math.floor(self._val)
def ceil(self):
return math.ceil(self._val)
def round(self, ndigits=None):
return round(self._val, ndigits=ndigits)
def abs(self):
return abs(self._val)
def floordiv(self, other):
return self._val // other
def rfloordiv(self, other):
return other // self._val
def truediv(self, other):
return self._val / other
def rtruediv(self, other):
return other / self._val
def mod(self, other):
return self._val % other
def rmod(self, other):
return other % self._val
def lt(self, other):
return self._val < other
def le(self, other):
return self._val <= other
def eq(self, other):
return float(self) == float(other)
def neg(self):
return ExtendedInteger(-self.a, -self.b, self.p)
def pos(self):
return ExtendedInteger(+self.a, +self.b, self.p)
def add(self, other):
if isinstance(other, ExtendedInteger):
# Если оба экземпляра имеют одно и то же значение p,
# вернуть новый экземпляр ExtendedInteger
if self.p == other.p:
new_a = self.a + other.a
new_b = self.b + other.b
return ExtendedInteger(new_a, new_b, self.p)
# В противном случае вернуть значение типа float
else:
return self._val + other._val
# Если other - значение класса Integral, прибавить значение other к значению self.a
elif isinstance(other, numbers.Integral):
new_a = self.a + other
return ExtendedInteger(new_a, self.b, self.p)
# Если other - значение класса Real, вернуть значение типа float
elif isinstance(other, numbers.Real):
return self._val + other._val
# Если тип other неизвестен, позволить другим принять решение
# о том, что делать в такой ситуации
else:
return NotImplemented
def radd(self, other):
# Сложение коммутативно, поэтому прибегнуть к add
return self.add(other)
def mul(self, other):
if isinstance(other, ExtendedInteger):
# Если оба экземпляра имеют одно и то же значение p,
# вернуть новый экземпляр ExtendedInteger
if self.p == other.p:
new_a = (self.a * other.a) + (self.b * other.b * self.p)
new_b = (self.a * other.b) + (self.b * other.a)
return ExtendedInteger(new_a, new_b, self.p)
# в противном случае вернуть значение типа float
else:
return self._val * other._val
# Если other - значение класса Integral, умножить его компоненты a и b на other
elif isinstance(other, numbers.Integral):
new_a = self.a * other
new_b = self.b * other
return ExtendedInteger(new_a, new_b, self.p)
# Если other - значение класса Real, вернуть значение типа float
elif isinstance(other, numbers.Real):
return self._val * other
# Если тип other неизвестен, позволить другим принять решение
# о том, что делать в такой ситуации
else:
return NotImplemented
def rmul(self, other):
# Умножение коммутативно, поэтому прибегнуть к mul
return self.mul(other)
def pow(self, exponent):
return self._val ** exponent
def rpow(self, base):
return base ** self._val
Для того чтобы обеспечить правильность реализации интерфейса Real конкретным типом — нужно создать реализации множества методов, в именах которых есть два символа подчёркивания. Ещё нужно поразмыслить о том, как методы вроде .add() и .mul() взаимодействуют с другими типами, являющимися наследниками Real.
Обратите внимание: вышеприведённый пример не создавался в расчёте на его полноту или абсолютную правильность. Его цель — продемонстрировать читателю возможности работы с числами.
При наличии реализации ExtendedInteger можно заниматься следующими вычислениями:
>>> a = ExtendedInteger(1, 2)
>>> b = ExtendedInteger(2, 3)
>>> a
ExtendedInteger(1, 2, 2)
>>> # Проверяем то, что a - это наследник Number
>>> isinstance(a, numbers.Number)
True
>>> # Проверяем то, что a - это наследник Real
>>> isinstance(a, numbers.Real)
True
>>> print(a)
1 + 2√2
>>> a * b
ExtendedInteger(14, 7, 2)
>>> print(a * b)
14 + 7√2
>>> float(a)
3.8284271247461903
Иерархия числовых типов в Python — довольно гибкая структура. Но, конечно, всегда стоит очень внимательно относиться к реализации типов, являющихся наследниками встроенных абстрактных базовых типов Python. Нужно обеспечить их корректную работу друг с другом.
В документации по Python можно найти несколько советов по реализации собственных типов, которые стоит прочесть тому, кто решит заняться созданием собственных числовых типов. Такому человеку ещё полезно будет ознакомиться с реализацией Fraction.
Итоги
Вот — те три особенности Python-чисел, которые мы здесь обсуждали:
У чисел есть методы, как и у практически всех остальных объектов в Python.
Числа обладают иерархией, даже несмотря на то, что их чёткие взаимоотношения несколько портит наличие типов
Decimalиfloat.Программисты могут создавать собственные числовые типы, которые вписываются в иерархию числовых типов Python.
Может быть, вы узнали из этого материала не только об этих особенностях чисел, но и ещё о чём-нибудь, что вам пригодится.
О, а приходите к нам работать? ????
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Комментарии (13)

Sergaza
24.01.2022 19:08+4«Вот — интересный метод .as_integer_ratio()»
Этот метод -- натуральная и ненужная ерунда. У меня, когда я его проверял, кажется, не было ни одного совпадения, когда я создавал вещественное число на основе дроби двух целых чисел, а потом пытался провести обратную операцию методом 'as_integer_ratio'. Пришлось писать собственную функцию на основе классического бесконечного алгоритма представления вещественного числа дробью из целых чисел.

GCU
26.01.2022 23:48Если под "создавал вещественное число" имеется ввиду float, то он по своему представлению может иметь в знаменателе только степени двойки. Соответственно все дроби с другим знаменателем будут представлены с округлением, с потерей точности. И любой алгоритм подбора дроби целых чисел из float, который даст в знаменателе не степень двойки - тоже округление и потеря точности от значения float.

Bhudh
25.01.2022 01:13+2Если обойтись без скобок — при попытке вызова метода на целочисленном литерале будет выдана ошибка SyntaxError.
>>> 255 .to_bytes(2, 'big') b'\x00\xff'

Chupaka
25.01.2022 12:49+2Например, у дроби 13 нет конечного десятичного представления (после десятичной точки идёт бесконечное множество троек). А у дроби 110 нет конечного представления в виде двоичной дроби.
Наверное, этот нумерованный список дробей тоже не вошёл в статью...

ShashkovS
25.01.2022 13:46+1В описании класса ExtendedInteger почти у всех методов пропущены двойные подчёркивания. Это же «магические» методы

arokettu
26.01.2022 16:59числа с плавающей точкой, представляющие суммы в некоей валюте
ОМГ, никогда не храните суммы в валюте в числах с плавающей точкой

igand
26.01.2022 17:00Кроме float("inf"), также есть float("-inf").
Еще в двух местах знак деления потерялся: "у дроби 13" -> "у дроби 1/3", и дальше аналогично про 1/10.

AndrewAtResearch
26.01.2022 17:00+1Один из способов представления числа 1729 в виде суммы двух кубов — это 13 + 123. Можете отыскать второй способ?
print([(x,y) for x in range(13) for y in range(x) if x*x*x+y*y*y==1729])[(10, 9), (12, 1)]

tenzink
26.01.2022 19:03Интересно, как ведёт себя функция
равная минимальному числу, разлагающемуся в сумму 2-х кубов
разными способами. Знаем, что
akomiagin
Спасибо, за интересный материал. Я бы еще добавил абзац про работу с натуальными дробями с помощью модуля fractions для полноты картины.