Приключения чисел в python и mypy или the numeric tower
- суббота, 13 августа 2022 г. в 00:37:34
Если вы когда-нибудь чувствовали, что вы погрязли в совещаниях и обсуждениях, которые всё длятся и длятся, а решения проблемы всё нет, знайте: в mypy есть 5-летний issue, о том что целое число не является числом.
Подсказки типов в python являются интересной темой. Люди из статически типизированных языков не понимают, как можно было создавать язык без них, а потом зачем-то их прикручивать, любители динамики, не понимают, зачем тратить время на добавление типов, если код и так работает, а анализаторы только на всякую ерунду ругаются. В то время как разработчики на python продолжают ковырять и дебажить код, пытаясь понять, что же имел ввиду автор и докидывая типы, если понять удалось.
Однако, прикручивание типов и их проверки где-то сбоку действительно имеет некоторые проблемы, для примера можно привести такую, вроде бы, простую тему как числа.
В python есть следующие встроенные типы для чисел: целые, вещественные с плавающей и фиксированной точкой, рациональные дроби и даже комплексные числа. Данные типы реализуют определённые интерфейсы (ABCs), организованные в numeric tower (Number, Complex, Real, Rational и Integral). И вот здесь, начинаются проблемы. Некоторые решения выглядят понятными и допустимыми, например, в функцию, принимающую float всегда можно передать int, а в функцию, принимающую complex всегда можно передать и float, и int.
def sin(a: float):
print(isinstance(a, float))
def cos(a: complex):
print(isinstance(a, complex))
sin(1) # выведет False
cos(4.5) # тоже выведет False
Проверка mypy выведет Success: no issues found in 1 source file.
И с математической точки зрения в этом есть смысл, любое вещественное число является комплексным, а целое — вещественным. Но есть пуристы, которые такой код не пропустят, например, в rust, нельзя передавать целые числа в функции, ожидающее вещественное число.
fn f(a: f32) {
}
fn main() {
f(4)
}
Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
--> src/main.rs:6:7
|
6 | f(4)
| - ^
| | |
| | expected `f32`, found integer
| | help: use a float literal: `4.0`
| arguments to this function are incorrect
Да и в python красивая математическая абстракция начинает течь, как можно заметить проверки типов проходят в mypy, но проверки isinstance
возвращают False. То есть тип в сигнатуре и в isinstance
не одно и то же, впрочем, это даже понятно, учитывая __subclasshook__
и динамизм языка.
Так же в python decimal и float, не являются взаимозаменяемыми и совместимыми, хотя с точки зрения математики, и то, и другое — вещественные числа, но создатели языка решили, что смешивать в одной операции два типа не стоит, так как это может привести к потере точности.
>>> Decimal(1) + 2.5
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'
mypy об этом знает и такие операции не разрешает:
ws.py:15: error: Unsupported operand types for + ("Decimal" and "float")
Так же, Decimal нельзя передать в функцию, ожидающую комплексную переменную, а целое число нельзя передать в функцию, ожидающую Decimal. Хотя математическая абстракция, по идее, не должна переставать работать от того, то мы поменяли вещественные числа с плавающей точкой на числа с фиксированной, они всё равно остаются вещественными и математические операции, допустимые над Decimal должны выполнять и над int. Но такой код mypy не пропустит.
from decimal import Decimal
from numbers import Number
def sin(a: Decimal):
...
def cos(a: complex):
...
sin(1)
cos(Decimal(1))
ws.py:13: error: Argument 1 to "sin" has incompatible type "int"; expected "Decimal"
ws.py:14: error: Argument 1 to "cos" has incompatible type "Decimal"; expected "complex"
Отдельный забавный момент состоит в том, что в python bool наследуется от int.
>>> int.__subclasses__()
[bool, ...
from decimal import Decimal
def sin(x: float):
pass
sin(Decimal(1)) # ws.py:7: error: Argument 1 to "sin" has incompatible type "Decimal"; expected "float"
sin(1==0) # А эти строчки
sin(True) # проверку проходят
То есть вычислить "синус" от 1 нельзя, а от истины — возможно.
Теперь вернёмся к КДПВ. int, Decimal и float являются Number.
isinstance(1, Number) # True
isinstance(2.5, Number) # True
isinstance(Decimal(2.5), Number) # True
Но, mypy так не считает.
from decimal import Decimal
from numbers import Number
def sin(x: Number):
pass
sin(Decimal(1))
sin(1)
sin(2.5)
sin(True)
ws.py:8: error: Argument 1 to "sin" has incompatible type "Decimal"; expected "Number"
ws.py:9: error: Argument 1 to "sin" has incompatible type "int"; expected "Number"
ws.py:10: error: Argument 1 to "sin" has incompatible type "float"; expected "Number"
ws.py:11: error: Argument 1 to "sin" has incompatible type "bool"; expected "Number"
sin(Number(1))
тоже не пройдёт, так как Number — абстрактный класс.
Python достаточно приятный в использовании язык, недаром он стал одним из самых популярных (а по некоторым подсчётам - самым популярным). И многие архитектурные решения, принятые при реализации языка и интерпретатора достойны изучения. Но, иногда даже такие простые сущности как числа приводят к необходимости принимать сложные и неоднозначные решения, допустив в процессе несколько багов и неочевидностей. Так что, если вдруг ваш начальник будет недоволен вашей архитектурой, можете попытаться отмазаться тем, что не только у вас не получается создать идеальную иерархию типов, даже у Гвидо не всегда получается.