Если вы когда-нибудь чувствовали, что вы погрязли в совещаниях и обсуждениях, которые всё длятся и длятся, а решения проблемы всё нет, знайте: в 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 достаточно приятный в использовании язык, недаром он стал одним из самых популярных (а по некоторым подсчётам - самым популярным). И многие архитектурные решения, принятые при реализации языка и интерпретатора достойны изучения. Но, иногда даже такие простые сущности как числа приводят к необходимости принимать сложные и неоднозначные решения, допустив в процессе несколько багов и неочевидностей. Так что, если вдруг ваш начальник будет недоволен вашей архитектурой, можете попытаться отмазаться тем, что не только у вас не получается создать идеальную иерархию типов, даже у Гвидо не всегда получается.
Комментарии (14)
Wesha
13.08.2022 05:54А решение-то — единственный кусок кода (псевдокод, ибо питонам не обучен):
def sin(int x)
sin(float::x)
endРаз требуется, чтобы параметр был определённого типа — пишете обёртку, которая берёт "любой" тип, и приводит его к "определённому".
san-smith
13.08.2022 10:00Честно говоря, не назвал бы это «решение» решением.
В общем случае это не работает — представьте, что «определённым» типом является int, а «любым» — float или complex. Уверены, что есть разумные правила приведения?
Кстати, что насчёт ситуации, когда приведение типа завершилось неудачно?
LinearLeopard Автор
13.08.2022 10:29Спасибо за интерес.
В python приведение типа работает только для анализатора docs.python.org/3/library/typing.html#typing.cast
Кроме того, оно приводит любой тип к любомуfrom typing import cast def s(x: str): pass s(cast(str, 1)) # Success: no issues found in 1 source file
Что равнозначно отсутствию проверки типов совсем.
Но как таковой проблемы нет, случаев, когда в одну ф-цию захочется передавать все эти типы очень немного, ничто не мешает в таких общих случаях тип не декларировать или перечислять все базовые типы:def f(x: Union[float, Decimal])?
Или объявить протокол:from decimal import Decimal from typing import cast, Protocol class MyNumber(Protocol): def __add__(self, other): pass def __mul__(self, other): pass def __pow__(self, power, modulo=None): pass def s(x: MyNumber): pass s(cast(str, 1)) s(Decimal(1))
Строка не пройдёт:ws.py:21: error: Argument 1 to "s" has incompatible type "str"; expected "MyNumber" ws.py:21: note: 'str' is missing following 'MyNumber' protocol member: ws.py:21: note: __pow__ Found 1 error in 1 file (checked 1 source file)
Правда, не пройдёт только из-за того, что у неё не определёна операция возведения в степень.
evtn
15.08.2022 12:17-1Если бы вы сначала разобрались, а не побежали бы поскорее писать статью на Хабр, вы бы написали статью совсем о другом, другой длины и с нормальным разбором конкретной темы. В текущем варианте вы прыгаете с одного вопроса на другой и смешиваете две разных системы.
Для начала стоило бы в статье оставить ссылку на само обсуждение.
Так называемый number (или numeric) tower изначально определён как модуль numbers в PEP 3141 от далёкого 2007 года. То есть за год до релиза Python 3 и за семь лет до появления тайпхинтов в 2014. Тот же самый PEP 484, прямо в том же абзаце, который вы привели в пример, предлагает использовать реальные классы для реализации numeric tower, а вы это предложение игнорируете и продолжаете использовать старые абстрактные классы.
Более того, давайте посмотрим в тот самый PEP 3141 и узнаем, как должен быть определён Number:class Number(metaclass=ABCMeta): pass
То есть, в общем-то, никаких методов нам этот класс не даёт (потому что это протокол для абстрактного числа, а число может иметь совершенно разные свойства и разные операции — к примеру, если вы принимаете два числа, вы не можете их сравнивать, потому что сравнение не определено для комплексных чисел). Что вы имеете в виду, когда пишете
x: Number
?
Давайте и дальше читать половину документации и бежать оформлять текст на Хабр из-за этого.LinearLeopard Автор
15.08.2022 12:36Спасибо за интерес к статье.
Статья написана в пятницу и представляет собой обычную не очень серьёзную пятничную хабра статью. Не стоит относиться к ней как к статье по теории типов в Oxford University Press или IEEE.
P.S. Ссылку забыл добавить, сейчас пофикшу, спасибо.evtn
15.08.2022 13:49Да я не говорю, что статья должна тянуть на серьёзное исследование, просто она какая-то пустая, что ли. Я за 5 минут пролистал обсуждение и пробежался по PEP 484/3141 достаточно, чтобы расписать суть лучше.
Хотелось бы, чтобы авторы статей на Хабре тоже разбирались в предмете статьи, хотя бы в виде 10-минутного прочтения литературы об этом.
vadimr
Как придумать самим себе проблему, типизировав формальный параметр, и затем её героически решать.
LinearLeopard Автор
Не совсем понял, о каком формальном параметре идёт речь. Если вы о функции sin, то реальная умеет работать со всеми типами:
и не типизирована docs.python.org/3/library/math.html#math.sin
Параметр только позиционный
Наверно, не стоило использовать название реальной ф-ции, но думал, кавычек в тексте хватит.
vadimr
def sin(x: Number):
Вопрос в том, зачем так было писать. Неважно, как эта функция называется.
Так-то уж, если по существу разбираться, элементарные типы вроде int по своей реализации не являются наследниками абстрактных классов.
LinearLeopard Автор
Зачем говорить, что ф-ция принимает число?
vadimr
Я не понимаю, что такое “число”. Это вредная генерализация понятия.
Со времён после Фортрана II договорились иногда разрешать использовать целые константы в качестве вещественных, но нельзя же этот весьма дурно пахнущий трюк эксплуатировать, расширяя до бесконечности.
LinearLeopard Автор
А, кажется, понял, вы имеете ввиду, что предпочли бы видеть
def f(x: Union[int, float, Decimal, Fraction, Complex])
?vadimr
Я бы предпочёл видеть f(x). А если уж хочется расписать типы, то тогда надо для начала разбираться, int там должно быть по существу задачи или float.
Бессмысленно специфицировать тип просто исходя из того, что технически в состоянии принять синус в конечном итоге. Это то же самое, что отсутствие спецификации, только запутаннее.
LinearLeopard Автор
Судя по тому, что баг за 5 лет так и не пофиксили, не вы не одиноки с данным мнением)