Как определить тип первого элемента в итерации?

Как определить тип первого элемента в итерации в 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.

Самое близкое, что вы можете сделать, - это добавить перегрузки кортежей - это единственная неоднородная (и, следовательно, в некоторых случаях имеющая статическую длину) последовательность в Python. Вы все равно можете вернуться к другим итерациям, чтобы вернуть T | None, но верните T для кортежа, содержащего T, следующего за моими 0 или более другими элементами. mypy-play.net/… (отредактировано: TypeVarTuple не требуется)

STerliakov 06.08.2024 20:06

(и выкинуть | List[T] — список является итерируемым, нет необходимости указывать его явно)

STerliakov 06.08.2024 20:10

Хорошие комментарии. Я думаю, вам следует переписать их как ответ на этот вопрос.

Vlad 06.08.2024 20:12
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
3
59
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Это невозможно. «Имеет первый элемент» не является частью статического типа ll. Чтобы определить, что first_bob не является None, потребуется более глубокий анализ, чем простая проверка статического типа.

Ааа, я вижу... хорошая мысль

Vlad 06.08.2024 20:13
Ответ принят как подходящий

Рассматриваемый код уже удовлетворяет части ваших требований: «...возвращает объект типа, который содержится в итерируемом...». Да, но необязательно (это или 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.

Другие вопросы по теме