Как определить тип первого элемента в итерации в MyPy/Pyright?
Есть ли способ аннотировать приведенный ниже код для более узкой области применения? Это означает, что я хочу, чтобы средство проверки типов предполагало, что приведенная ниже функция возвращает объект типа, который содержится в итерации или списке.
from typing import Iterable, TypeVar
T = TypeVar('T')
def get_first(iterable: Iterable[T]) -> T | None:
for item in iterable:
return item
return None
Например:
class Bob:
pass
ll : List[Bob] = [Bob(), Bob()] # Create a list that contains objects of type Bob
first_bob = get_first(ll) # <--- Type checker should infer that the type of first_bob is Bob
Я ищу решение, которое работает специально в MyPy/Pyright.
(и выкинуть | List[T]
— список является итерируемым, нет необходимости указывать его явно)
Хорошие комментарии. Я думаю, вам следует переписать их как ответ на этот вопрос.
Это невозможно. «Имеет первый элемент» не является частью статического типа ll
. Чтобы определить, что first_bob
не является None
, потребуется более глубокий анализ, чем простая проверка статического типа.
Ааа, я вижу... хорошая мысль
Рассматриваемый код уже удовлетворяет части ваших требований: «...возвращает объект типа, который содержится в итерируемом...». Да, но необязательно (это или None
), потому что в пустой итерации нет записей.
Если вы хотите выразить «имеет хотя бы один элемент», вам нужна конструкция типизации, которая поддерживает какое-то ограничение длины. В питоне есть такая конструкция: tuple
. Это единственная последовательность, которая может быть неоднородной, и единственная, которая может иметь фиксированную длину (например, вы можете статически выразить «x должен быть кортежем из двух»). Другие конструкции (Iterable, Sequence, List, любой пользовательский тип, не являющийся подклассом tuple
) не говорят о длине, для них длина — это просто __len__
метод, означающий «есть способ получить какое-то значение из len(x)
вызова».
Таким образом, вы можете гарантировать «Т» только для типов tuple
, указав, что у них есть хотя бы один элемент. Любая другая итерация потребует возврата к T | None
, поскольку она может быть пустой.
from collections.abc import Iterable
from typing import TypeVar, overload
T = TypeVar('T')
@overload
def get_first(iterable: tuple[T, *tuple[object, ...]]) -> T: ...
@overload
def get_first(iterable: Iterable[T]) -> T | None: ...
def get_first(iterable: Iterable[T]) -> T | None:
for item in iterable:
return item
return None
И использование:
reveal_type(get_first((1,))) # int
reveal_type(get_first((1, 'a', 'b'))) # int
reveal_type(get_first([])) # Error (unknown type variable)
t: list[int] = []
reveal_type(get_first(t)) # int | None
reveal_type(get_first([1])) # int | None
игровая площадка mypy (судя по комментарию, сейчас кажется не в сети — надеюсь, она скоро восстановится), игровая площадка пирайт.
Обратите внимание, что пользователям необходимо будет передать реальный кортеж известной длины (а не, например, tuple[int, ...]
) — его можно либо создать как литерал, либо вернуть из какой-либо другой функции, аннотированной для возврата N-кортежа.
Обратите внимание, что это почти «фундаментальное» ограничение. Haskell имеет очень сильную систему типов, поддерживающую широкий спектр операций с типами, но ее head
является частичной: у нее есть сигнатура [a] -> a
, и она завершается с исключением, если ввод представляет собой пустой список. Исключение означает «не вернулось», так что для системы типов это нормально — вы можете сделать то же самое в Python (s/return None/raise ValueError), и у вас больше не будет проблем с None. Если вы хотите иметь «безопасный» head
, вам нужно как-то его определить, возможно, используя монаду Maybe
(так же, как Option
в Rust, аналогично | None
в Python).
Попытки написать «непустую коллекцию» существуют, но в основном они реализуются через отдельный тип, который проверяет контейнер во время выполнения (например, в Swift) и обертывает его: вы можете объявить свой собственный «NonEmptyList» и принять его, и потребителям придется где-то создавать его вручную.
В TypeScript есть решение, но, скорее всего, это просто потому, что массивы и кортежи там очень похожи. Он по-прежнему требует явных проверок, но в вашем конкретном случае было бы очень полезно, предоставив дополнительную перегрузку для таких типов без undefined
.
Самое близкое, что вы можете сделать, - это добавить перегрузки кортежей - это единственная неоднородная (и, следовательно, в некоторых случаях имеющая статическую длину) последовательность в Python. Вы все равно можете вернуться к другим итерациям, чтобы вернуть
T | None
, но верните T для кортежа, содержащего T, следующего за моими 0 или более другими элементами. mypy-play.net/… (отредактировано: TypeVarTuple не требуется)