Как мне ввести целочисленную переменную, которая также может быть бесконечной?

В поисках этой темы я наткнулся на следующее: Как представить целочисленную бесконечность?

Я согласен с Мартином Петерсом в том, что добавление отдельного специального значения бесконечности для int может быть не лучшей идеей.

Однако это затрудняет хинтинг типов. Предположим следующий код:

myvar = 10   # type: int
myvar = math.inf  # <-- raises a typing error because math.inf is a float

Однако код везде ведет себя так, как должен. И мой тип-хинтинг верен везде.

Если вместо этого я напишу следующее:

myvar = 10  # type: Union[int, float]

Я могу назначить math.inf без сучка и задоринки. Но теперь принимаются и любые другие поплавки.

Есть ли способ правильно ограничить подсказку типа? Или я вынужден использовать type: ignore каждый раз, когда назначаю бесконечность?


person exhuma    schedule 20.02.2019    source источник
comment
Я не большой поклонник подклассов int. Если не осторожно, то легко сломать вещи. И не стоит рисковать. И чтобы ответить на вопрос, что и почему: я имею дело с ограничениями скорости сервисных маршрутизаторов. Они указываются в кбит / с (целое число). Но они также могут быть неограниченными (что соответствует бесконечному пределу).   -  person exhuma    schedule 20.02.2019
comment
Один из возможных грязных приемов может заключаться в том, чтобы сделать что-то вроде inf = cast(int, math.inf), а затем начать везде использовать вашу пользовательскую константу inf вместо math.inf. Однако я согласен с анализом @metatoaster здесь: я думаю, что все решения, которые он или она обсуждали, более чистые и надежные, чем то, что я представил.   -  person Michael0x2a    schedule 22.02.2019
comment
@metatoaster Я тоже согласен с вашей оценкой. Если вам удастся сформулировать это в виде ответа, я его приму.   -  person exhuma    schedule 23.02.2019


Ответы (1)


Супер ленивое (и, вероятно, неправильное) решение:

Вместо того, чтобы добавлять конкретное значение, класс int может быть расширен путем создания подклассов. Этот подход не лишен ряда ловушек и проблем, таких как требование обрабатывать значение бесконечности для различных __dunder__ методов (т.е. __add__, __mul__, __eq__ и т.п., и все они должны быть протестированы). Это было бы неприемлемым объемом накладных расходов в случаях использования, когда требуется конкретное значение. В таком случае упаковка желаемого значения с помощью typing.cast будет иметь возможность лучше указать системе подсказок типов, какое конкретное значение (например, inf = cast(int, math.inf)) приемлемо для присвоения.

Причина, по которой этот подход неверен, проста в следующем: поскольку назначенное значение выглядит / ощущается как некоторое число, некоторые другие пользователи вашего API могут случайно использовать это как int, и тогда программа может сильно взорваться, когда math.inf ( или их вариации).

Аналогия заключается в следующем: учитывая, что в списках есть элементы, индексированные положительными целыми числами, мы ожидаем, что любая функция, возвращающая индекс для некоторого элемента, будет некоторым положительным целым числом, поэтому мы можем использовать его напрямую (я знаю, что это не так в Python учитывая, что есть семантика, которая позволяет использовать отрицательные значения индекса, но представьте, что мы работаем, скажем, с C на данный момент). Скажем, эта функция возвращает первое вхождение совпадающего элемента, но если есть какие-либо ошибки, она возвращает некоторое отрицательное число, которое явно превышает диапазон допустимых значений для индекса для некоторого элемента. Отсутствие защиты от наивного использования возвращаемого значения неизбежно приведет к проблемам, которые должна решать система типов.

По сути, создание суррогатных значений и отметка их как int будет предлагать нулевое значение и неизбежно позволит автоматически проявить неожиданные и неработающие API / поведение программы из-за неправильного использования.

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

В качестве альтернативы, посмотрите str.index vs _ 14_. Один из них имеет возвращаемое значение, которое определенно нарушает ожидания пользователя (т.е. превышает границы типа положительное целое число; не будет сказано, что возвращаемое значение может быть недопустимым для контекста, в котором оно может использоваться во время компиляции, приводит к потенциальный отказ случайным образом во время выполнения).

Формулировка вопроса / ответа в более правильных терминах:

Учитывая, что проблема действительно связана с присвоением некоторого целого числа при наличии скорости, и если таковой не существует, следует использовать какой-либо другой токен, который представляет неограниченность для конкретного варианта использования (это может быть какое-то встроенное значение, такое как NotImplemented или None). Однако, поскольку эти токены также не будут int значениями, это означает, что myvar действительно потребуется тип, который их охватывает, и способ применения операции, которая будет делать правильные вещи.

К сожалению, это не очень хорошо доступно напрямую в Python, однако в языках со строго статической типизацией, таких как Haskell, более приемлемым решением является использование _ 19_ для определения типа числовой тип, допускающий бесконечность. Обратите внимание, что хотя там также доступна бесконечность с плавающей запятой, она наследует все проблемы чисел с плавающей запятой, что делает это несостоятельным решением (опять же, не используйте для этого inf).

Вернемся к Python: в зависимости от свойства назначения, которое вы действительно хотите, это может быть так же просто, как создание класса с конструктором, который может принимать int или None (или NotImplemented), а затем предоставить метод, который пользователи класс может использовать фактическое значение. К сожалению, Python не предоставляет расширенные конструкции, чтобы сделать это элегантным, поэтому вы неизбежно получите код, управляющий этим, будет разбрызгиваться повсюду, или вам придется написать ряд методов, которые обрабатывают любой ввод, как ожидалось, и производят требуемый вывод в конкретными способами, которые актуальны для вашей программы.

К сожалению, подсказка типов на самом деле только царапает поверхность и просто перебирает то, что более продвинутые языки предоставили и решили на более фундаментальном уровне. Я предположил, что если нужно программировать на Python, это лучше, чем не иметь его.

person metatoaster    schedule 24.02.2019
comment
В моем случае на самом деле полезно различать значение настроено и абсолютное, значение настроено и неограничено, а значение не задано. Так что, возможно, использование чего-то вроде Union[int, MyInfinite, None] тоже может помочь. Но тогда я буду вынужден заниматься этим везде. Но, как вы говорите, это та область, где Python немного не хватает. - person exhuma; 25.02.2019
comment
В идеале вам определенно нужен новый тип, который объединяет все это, чтобы пользователи этого атрибута не делали предположений о типе, который в настоящее время присутствует там, если они могут увидеть назначенный ему int. Учитывая, что mypy может быть необязательным и не использоваться наивным зависимым от пакета, содержащего этот атрибут с указанием типа, разработчик может увидеть 1000 в myvar, а затем получить формулу myvar * 10, которая взорвется позже (несмотря на то, что mypy действительно помечает эту операцию как недействительные, так как они являются объединениями типов). В конечном итоге это зависит от вашего конкретного варианта использования. - person metatoaster; 25.02.2019
comment
Я согласен. Это внутренний тип, и мы применяем mypy в нашей команде. Так что в целом это не такая уж большая проблема. Но я согласен, что это можно было бы завернуть, чтобы избежать несчастных случаев. - person exhuma; 25.02.2019