Я хочу перегрузить функцию ниже, чтобы, если передано значение, которое поддерживает int()
подсказки типа Python int
в противном случае тип Python подсказывает переданное значение.
Модуль Python typing
предоставляет тип SupportsInt
, который мы можем использовать, чтобы проверить, поддерживает ли наше значение int.
from typing import Any, SupportsInt, overload
@overload
def to_int(value: SupportsInt) -> int: ...
@overload
def to_int[T: NotSupportsInt???](value: T) -> T: ...
def to_int(value: Any) -> Any:
try:
return int(value)
except TypeError:
return value
Но как мы можем указать во втором утверждении overload
все значения, которые не поддерживают int
?
Вы можете реализовать так:
Union, чтобы разрешить SupportsInt или любой другой тип.
Используйте переменную типа T и Union, чтобы указать, что вторая перегрузка может принимать любой тип.
Во второй перегрузке мы указываем, что тип возвращаемого значения может быть либо int, либо того же типа, что и входное значение.
Таким образом, если значение поддерживает int, оно будет преобразовано в int, в противном случае значение будет возвращено как есть.
from typing import Any, SupportsInt, TypeVar, Union, overload
T = TypeVar('T')
@overload
def to_int(value: SupportsInt) -> int: ...
@overload
def to_int(value: T) -> T: ...
def to_int(value: Union[SupportsInt, T]) -> Union[int, T]:
try:
return int(value)
except TypeError:
return value
Типы дают позитивные обещания — они определяют, что может делать объект. Не то, что объект не может сделать.
Если у вас есть объект статического типа float
, вы знаете, что этот объект поддерживает __int__
, поэтому вы знаете, что он является членом типа SupportsInt
.
Если у вас есть объект статического типа object
, object
не имеет метода __int__
, но этот объект все равно может поддерживать __int__
. Объектом может быть число с плавающей запятой или экземпляр какого-либо другого подкласса object
, поддерживающего __int__
. Вы не знаете, вернет ли передача to_int
целое число.
Ваша вторая перегрузка пытается сказать: «Не поддерживает __int__
? Тогда верните исходный тип». Он должен сказать: «Не знаю, поддерживает ли он __int__
? Тогда, возможно, верните исходный тип или, возможно, вернете int
». Вы бы сделали это с помощью переменной неограниченного типа и возвращаемого типа объединения, например:
from typing import overload, SupportsInt, TypeVar
T = TypeVar('T')
@overload
def to_int(value: SupportsInt) -> int: ...
@overload
def to_int(value: str) -> int: ...
@overload
def to_int(value: T) -> T | int: ...
def to_int(value):
try:
return int(value)
except TypeError:
return value
Обратите внимание, что я также добавил отдельную перегрузку для str
. str
на самом деле не поддерживает __int__
, поэтому не попадает под перегрузку SupportsInt
— вызовы типа int('35')
обрабатываются специальным случаем в int.__new__
.
И конечно, иногда у вас может возникнуть случай, подобный to_int([])
, когда вы знаете конкретный тип объекта и знаете, что вызов int
завершится неудачно. Но система типов не распространяет эту информацию так, как это необходимо для работы более конкретных аннотаций. Для средства проверки типов экземпляр list
может быть экземпляром какого-то странного подкласса __int__
, поддерживающего list
, даже если человек, смотрящий на код, видит, что это не так.
К сожалению, система аннотаций типов Python в настоящее время не имеет хорошего способа указания отрицательных типов. Вот что вы можете сделать:
from typing import Any, SupportsInt, TypeVar, Union, overload, cast
T = TypeVar('T')
# Option 1: downside: slight runtime penalty
def to_int_option_1(value: T) -> Union[int, T]:
try:
return int(cast(SupportsInt, value))
except TypeError:
return value
# Option 2: downside: ignoring type information, you'll need to make sure it's not hiding any bugs
def to_int_option_2(value: T) -> Union[int, T]:
try:
return int(value) # type: ignore
except TypeError:
return value
Это немного шире, чем тот тип, который вы ищете, но, вероятно, это придется сделать (учитывая, что перекрывающиеся подписи с несовместимыми типами возврата, как предложено в других ответах, не поддерживаются).
Это может сделать использование вашей функции немного неудобным, поскольку она не сообщает вам тип возвращаемого значения в зависимости от того, является ли аргумент SupportsInt
или нет. Например:
foo = to_int('37')
reveal_type(foo) # str | int, expected: int
bar = to_int([37])
reveal_type(bar) # list[int] | int, expected: list[int]
... а это значит, что вам, возможно, придется вставить такие символы, как assert isinstance(foo, int)
или typing.cast(list[int], bar)
, чтобы проверка типов работала правильно.
Вы можете добиться большего с перегрузкой, если правильно ее напишете — перекрытие сигнатур с совместимыми типами возвращаемых значений допустимо. Мой ответ перегружен пройти проверку типа.
Ваше может быть лучше с точки зрения документации, но с точки зрения проверки типов наши решения кажутся эквивалентными (например, reveal_type(to_int('37'))
печатает str | int
и reveal_type(to_int([37]))
печатает list[int] | int
)
Но reveal_type(to_int(3.5))
дает int
для моей версии и int | float
для вашей.
Я думаю, вы запутались, потому что str
на самом деле не поддерживает __int__
- особые случаи конструктора int
str
. (Если подумать, было бы неплохо добавить для этого отдельную перегрузку.)
А, я об этом не подумал, ты прав. Ваш ответ кажется наиболее близким к поведению, которое хочет Мэтт, и которое возможно с текущими функциями типизации mypy и python.
Привет, Мэтт! Система подсказок типов Python не предоставляет встроенного способа прямого выражения этой концепции. Но вы можете добиться того, чего хотите, используя комбинацию подсказок типов и универсальных типов.