- PVSM.RU - https://www.pvsm.ru -
Если вы писали код на Python, то весьма высока вероятность того, что вы, хотя бы в одной из своих программ, пользовались числами. Например, это могли быть целые числа для указания индекса значения в списке, или числа с плавающей точкой, представляющие суммы в некоей валюте.
Но числа в Python — это гораздо больше, чем, собственно, их числовые значения. Поговорим о трёх особенностях чисел в Python, с которыми вы, возможно, не знакомы.
В Python практически всё — это объект. Один из первых объектов, о котором узнаёт тот, кто начинает изучать Python — это str
, используемый для представления строк. Возможно, вы сталкивались с использованием методов строк, вроде .lower()
, который возвращает новую строку, все символы которой приведены к нижнему регистру:
>>> "HELLO".lower()
'hello'
Числа в Python тоже, как и строки, являются объектами. У них тоже есть методы. Например, целое число можно преобразовать в байтовую строку [1] с помощью метода [2] .to_bytes()
:
>>> n = 255
>>> n.to_bytes(length=2, byteorder="big")
b'x00xff'
Параметр 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
Преобразовать байтовую строку в целое число можно, воспользовавшись методом [3] .from_bytes()
класса int
:
>>> int.from_bytes(b'x06xc1', byteorder="big")
1729
Методы класса вызывают, используя имя класса, а не его экземпляр. Именно поэтому в предыдущем примере метод .from_bytes()
вызывают, обращаясь к int
.
Любопытный факт: 1729 — это самое маленькое положительное число, которое можно представить в виде суммы кубов двух положительных чисел двумя способами. Исторический анекдот [4] связывает это число с индийским математиком Сринивасой Рамануджаном [5], который рассказал о нём своему наставнику Готфри Харолду Харди [6].
Харди часто навещал Рамануджана, когда тот, умирая, находился в больнице в Патни. Именно в одно из таких посещений произошёл «инцидент» с номером такси. Харди приехал в Патни на такси, воспользовавшись своим излюбленным транспортным средством. Он вошёл в палату, где лежал Рамануджан. Начинать разговор Харди всегда было мучительно трудно, и он произнёс свою первую фразу: «Если не ошибаюсь, то номер такси, на котором я приехал, 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)
Правда, из-за ошибки представления чисел с плавающей точкой [7], иногда этот метод возвращает неожиданные результаты:
>>> 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-типов можно найти в документации [8].
В математике числа обладают естественной иерархией. Например, все натуральные числа являются целыми, а все целые числа — рациональными. Все рациональные числа — это вещественные числа, а все вещественные числа — это комплексные числа.
Похожие рассуждения применимы и к представлению чисел в Python. Здесь «числовая башня» выражается через абстрактные типы [9], содержащиеся в модуле [10] 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-чисел имеется пара своеобразных моментов.
Как уже было сказано, в «числовой башне» Python есть 4 конкретных числовых типа, соответствующих четырём абстрактным типам: complex
, float
, Fraction
и int
. Но в Python имеется и пятый числовой тип, представленный классом [11] 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 [12]:
Объекты 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
.
Получается, что «не числа» — это числа.
В общем, числа с плавающей точкой — странные создания.
Абстрактный числовой базовый тип 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
конкретным типом — нужно создать реализации множества методов [13], в именах которых есть два символа подчёркивания. Ещё нужно поразмыслить о том, как методы вроде .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. Нужно обеспечить их корректную работу друг с другом.
В документации [14] по Python можно найти несколько советов по реализации собственных типов, которые стоит прочесть тому, кто решит заняться созданием собственных числовых типов. Такому человеку ещё полезно будет ознакомиться с реализацией [15] Fraction
.
Вот — те три особенности Python-чисел, которые мы здесь обсуждали:
У чисел есть методы, как и у практически всех остальных объектов в Python.
Числа обладают иерархией, даже несмотря на то, что их чёткие взаимоотношения несколько портит наличие типов Decimal
и float
.
Программисты могут создавать собственные числовые типы, которые вписываются в иерархию числовых типов Python.
Может быть, вы узнали из этого материала не только об этих особенностях чисел, но и ещё о чём-нибудь, что вам пригодится.
Мы в wunderfund.io [16] занимаемся высокочастотной алготорговлей [17] с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Автор:
mr-pickles
Источник [19]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/371540
Ссылки в тексте:
[1] байтовую строку: https://docs.python.org/3/library/stdtypes.html#bytes
[2] метода: https://docs.python.org/3/library/stdtypes.html#int.to_bytes
[3] методом: https://docs.python.org/3/library/stdtypes.html#int.from_bytes
[4] Исторический анекдот: https://ru.wikipedia.org/wiki/1729_(%D1%87%D0%B8%D1%81%D0%BB%D0%BE)
[5] Сринивасой Рамануджаном: https://ru.wikipedia.org/wiki/%D0%A1%D1%80%D0%B8%D0%BD%D0%B8%D0%B2%D0%B0%D1%81%D0%B0_%D0%A0%D0%B0%D0%BC%D0%B0%D0%BD%D1%83%D0%B4%D0%B6%D0%B0%D0%BD_%D0%90%D0%B9%D0%B5%D0%BD%D0%B3%D0%BE%D1%80
[6] Готфри Харолду Харди: https://ru.wikipedia.org/wiki/%D0%A5%D0%B0%D1%80%D0%B4%D0%B8,_%D0%93%D0%BE%D0%B4%D1%84%D1%80%D0%B8_%D0%A5%D0%B0%D1%80%D0%BE%D0%BB%D0%B4
[7] ошибки представления чисел с плавающей точкой: https://docs.python.org/3/tutorial/floatingpoint.html
[8] документации: https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex
[9] абстрактные типы: https://docs.python.org/3/glossary.html#term-abstract-base-class
[10] модуле: https://docs.python.org/3/library/numbers.html
[11] классом: https://docs.python.org/3/library/decimal.html
[12] исходном коде CPython: https://github.com/python/cpython/blob/9e78dc25179a492550dc602e47e7f4d24e3c89a3/Lib/numbers.py#L24-L30
[13] методов: https://docs.python.org/3/reference/datamodel.html#special-method-names
[14] документации: https://docs.python.org/3/library/numbers.html#notes-for-type-implementors
[15] реализацией: https://github.com/python/cpython/blob/main/Lib/fractions.py
[16] wunderfund.io: http://wunderfund.io/
[17] высокочастотной алготорговлей: https://en.wikipedia.org/wiki/High-frequency_trading
[18] Присоединяйтесь к нашей команде.: http://wunderfund.io/#join_us
[19] Источник: https://habr.com/ru/post/647331/?utm_source=habrahabr&utm_medium=rss&utm_campaign=647331
Нажмите здесь для печати.