Введите подсказку декоратора/дескриптора, который передает внешний экземпляр во вложенный класс в качестве первого позиц ⇐ Python
-
Anonymous
Введите подсказку декоратора/дескриптора, который передает внешний экземпляр во вложенный класс в качестве первого позиц
У меня есть небольшой фрагмент кода, который должен быть декоратором/дескриптором, который передает внешний экземпляр внутреннему классу в качестве первого позиционного аргумента при доступе к нему из экземпляра. Поведение во время выполнения не имеет значения; Я спрашиваю конкретно о поведении двух типов шашек: mypy (игровая площадка) иpyright (игровая площадка).
из аннотаций импорта __future__ от ввода импорта TypeVar, Generic, ParamSpec, Concatenate, перегрузки, приведения из коллекций.abc import Callable из functools импорт частичного О = ТипВар('О') Я = ТипВар('Я') P = ПарамСпец('P') класс InnerClass(Generic[O, I, P]): _cls: Callable[Объединить[O, P], I] def __init__(self, внутренний_класс: Callable[Concatenate[O, P], I], /) -> Нет: self._cls = внутренний_класс @перегрузка def __get__(self, экземпляр: None, владелец: type[O] | None = None) -> Callable[Concatenate[O, P], I]: ... @перегрузка def __get__(self, экземпляр: O, владелец: тип[O] | None = None) -> Callable[P, I]: ... защита __get__( себя, пример: О | Никто, владелец: тип[O] | Нет = Нет ) -> Callable[Объединить[O, P], I] | Вызываемый[P, I]: если экземпляр равен Нет: вернуть self._cls return cast(Callable[P, I], частичное(self._cls, экземпляр)) Примеры В первых трех примерах используются внутренние классы, ожидаемый первый аргумент которых не является экземпляром соответствующих внешних классов. Я ожидал, что проверка типов завершится неудачно при использовании декоратора.
Что касается первого и второго случая, похоже, что пирайт добрался до сигнатур метода __get__, созданных @overload, и в конечном итоге потерпел неудачу. mypy был более снисходительным/беззаботным и разрешал исходную подпись при обоих доступах, из класса и из экземпляра.
класс _1: @InnerClass # mypy => хорошо # пирайт => хорошо # Ожидаемая => ошибка класс FirstArgIsNotOuter: def __init__(self, not_a: int, foo: str, *, bar: float) -> None: ... показ_типа (_1.FirstArgIsNotOuter) show_type(_1().FirstArgIsNotOuter) # mypy => (not_a: int, foo: str, *, bar: float) -> _1.FirstArgIsNotOuter # пирайт => ошибка: # Невозможно получить доступ к элементу "FirstArgIsNotOuter" для типа "_1"/"type[_1]" # Не удалось вызвать метод "__get__" для класса дескриптора "InnerClass[int, FirstArgIsNotOuter, (foo: str, *, bar: float)]" класс _2: @InnerClass # mypy => хорошо # пирайт => хорошо класс FirstArgIsNotOuterAndPositionalOnly: def __init__(self, not_a: int, /, foo: str, *, bar: float) -> None: ... показ_типа (_2.FirstArgIsNotOuterAndPositionalOnly) show_type(_2().FirstArgIsNotOuterAndPositionalOnly) # mypy => (int, foo: str, *, bar: float) -> _2.FirstArgIsNotOuterAndPositionalOnly # пирайт => ошибка: # Невозможно получить доступ к элементу "FirstArgIsNotOuterAndPositionalOnly" для типа "_2"/"type[_2]" # Не удалось вызвать метод "__get__" для класса дескриптора "InnerClass[int, FirstArgIsNotOuterAndPositionalOnly, (foo: str, *, bar: float)]" Третий пример с аргументами, содержащими только ключевые слова, показывает, что средства проверки типов определили, что сигнатуры конфликтуют с Concatenate[], который не может представлять аргументы с ключевыми словами:
класс _3: @InnerClass # mypy => Аргумент 1 для «InnerClass» имеет несовместимый тип «type[FirstArgIsNotOuterAndKeywordOnly]»; ожидается "Callable[[Never, NamedArg(int, 'not_a'), NamedArg(str, 'foo'), NamedArg(float, 'bar')], FirstArgIsNotOuterAndKeywordOnly]" # пирайт => ошибка: # Аргумент типа "type[FirstArgIsNotOuterAndKeywordOnly]" нельзя присвоить параметру "inner_class" типа "(O@InnerClass, **P@InnerClass) -> I@InnerClass" в функции "__init__" # Тип "type[FirstArgIsNotOuterAndKeywordOnly]" нельзя назначить типу "(O@InnerClass, **P@InnerClass) -> I@InnerClass" класс FirstArgIsNotOuterAndKeywordOnly: def __init__(self, *, not_a: int, foo: str, bar: float) -> None: ... С другой стороны, результаты для вызовов reveal_type() были разными: сначала пирайт правильно определил, что произошла ошибка доступа к классу, но каким-то образом пропустил доступ к экземпляру.< /п> reveal_type(_3.FirstArgIsNotOuterAndKeywordOnly) show_type(_3().FirstArgIsNotOuterAndKeywordOnly) # mypy => (*, not_a: int, foo: str, bar: float) -> _3.FirstArgIsNotOuterAndKeywordOnly # пирайт => ошибка: # Тип «FirstArgIsNotOuterAndKeywordOnly» частично неизвестен. # (Неизвестно, foo: str, bar: float) -> FirstArgIsNotOuterAndKeywordOnly # => нет ошибки: # (foo: str, bar: float) -> FirstArgIsNotOuterAndKeywordOnly Последние три примера во многом аналогичны приведенным выше, за исключением того, что ожидаемые первые аргументы имеют правильный внешний тип. mypy по-прежнему не смог обнаружить правильные подписи, но Pyright был хорош (за исключением последнего примера, где он допустил ту же ошибку, что и в примере 3).
класс _4: @InnerClass # mypy + piright => хорошо класс FirstArgIsOuter: def __init__(self, c: _4, foo: str, *, bar: float) -> None: ... показ_типа (_4.FirstArgIsOuter) show_type(_4().FirstArgIsOuter) # mypy => (c: _4, foo: str, *, bar: float) -> _4.FirstArgIsOuter #pyright => (_4, foo: str, *, bar: float) -> FirstArgIsOuter # => (foo: str, *, bar: float) -> FirstArgIsOuter класс _5: @InnerClass класс FirstArgIsOuterAndPositionalOnly: def __init__(self, c: _5, /, foo: str, *, bar: float) -> Нет: ... показ_типа (_5.FirstArgIsOuterAndPositionalOnly) show_type(_5().FirstArgIsOuterAndPositionalOnly) # mypy => (_5, foo: str, *, bar: float) -> _5.FirstArgIsOuterAndPositionalOnly #pyright => (_5, foo: str, *, bar: float) -> FirstArgIsOuterAndPositionalOnly # => (foo: str, *, bar: float) -> FirstArgIsOuterAndPositionalOnly класс _6: @InnerClass # mypy + piright => ошибка класс FirstArgIsOuterAndKeywordOnly: def __init__(self, *, c: _6, foo: str, bar: float) -> None: ... показ_типа (_6.FirstArgIsOuterAndKeywordOnly) show_type(_6().FirstArgIsOuterAndKeywordOnly) # mypy => (*, c: _6, foo: str, bar: float) -> _6.FirstArgIsOuterAndKeywordOnly # пирайт => ошибка: # Тип «FirstArgIsOuterAndKeywordOnly» частично неизвестен. # (Неизвестно, foo: str, bar: float) -> FirstArgIsOuterAndKeywordOnly # => нет ошибки: # (foo: str, bar: float) -> FirstArgIsOuterAndKeywordOnly Вопрос Есть ли способ улучшить этот фрагмент кода с точки зрения подсказок типов? Я бы хотел, чтобы средства проверки типов выходили из строя раньше, как указано. Если это невозможно, то, по крайней мере, я хочу лучших результатов от mypy. Объяснение ошибки пирайта в примерах 3 и 6 было бы плюсом, но не обязательно.
Я использую Python 3.12 (со старым синтаксисом общего класса, поскольку mypy еще не поддерживает PEP 695) с флагами --strict как для mypy 1.8.0, так и дляpyright 1.1.344 , если это имеет значение.
У меня есть небольшой фрагмент кода, который должен быть декоратором/дескриптором, который передает внешний экземпляр внутреннему классу в качестве первого позиционного аргумента при доступе к нему из экземпляра. Поведение во время выполнения не имеет значения; Я спрашиваю конкретно о поведении двух типов шашек: mypy (игровая площадка) иpyright (игровая площадка).
из аннотаций импорта __future__ от ввода импорта TypeVar, Generic, ParamSpec, Concatenate, перегрузки, приведения из коллекций.abc import Callable из functools импорт частичного О = ТипВар('О') Я = ТипВар('Я') P = ПарамСпец('P') класс InnerClass(Generic[O, I, P]): _cls: Callable[Объединить[O, P], I] def __init__(self, внутренний_класс: Callable[Concatenate[O, P], I], /) -> Нет: self._cls = внутренний_класс @перегрузка def __get__(self, экземпляр: None, владелец: type[O] | None = None) -> Callable[Concatenate[O, P], I]: ... @перегрузка def __get__(self, экземпляр: O, владелец: тип[O] | None = None) -> Callable[P, I]: ... защита __get__( себя, пример: О | Никто, владелец: тип[O] | Нет = Нет ) -> Callable[Объединить[O, P], I] | Вызываемый[P, I]: если экземпляр равен Нет: вернуть self._cls return cast(Callable[P, I], частичное(self._cls, экземпляр)) Примеры В первых трех примерах используются внутренние классы, ожидаемый первый аргумент которых не является экземпляром соответствующих внешних классов. Я ожидал, что проверка типов завершится неудачно при использовании декоратора.
Что касается первого и второго случая, похоже, что пирайт добрался до сигнатур метода __get__, созданных @overload, и в конечном итоге потерпел неудачу. mypy был более снисходительным/беззаботным и разрешал исходную подпись при обоих доступах, из класса и из экземпляра.
класс _1: @InnerClass # mypy => хорошо # пирайт => хорошо # Ожидаемая => ошибка класс FirstArgIsNotOuter: def __init__(self, not_a: int, foo: str, *, bar: float) -> None: ... показ_типа (_1.FirstArgIsNotOuter) show_type(_1().FirstArgIsNotOuter) # mypy => (not_a: int, foo: str, *, bar: float) -> _1.FirstArgIsNotOuter # пирайт => ошибка: # Невозможно получить доступ к элементу "FirstArgIsNotOuter" для типа "_1"/"type[_1]" # Не удалось вызвать метод "__get__" для класса дескриптора "InnerClass[int, FirstArgIsNotOuter, (foo: str, *, bar: float)]" класс _2: @InnerClass # mypy => хорошо # пирайт => хорошо класс FirstArgIsNotOuterAndPositionalOnly: def __init__(self, not_a: int, /, foo: str, *, bar: float) -> None: ... показ_типа (_2.FirstArgIsNotOuterAndPositionalOnly) show_type(_2().FirstArgIsNotOuterAndPositionalOnly) # mypy => (int, foo: str, *, bar: float) -> _2.FirstArgIsNotOuterAndPositionalOnly # пирайт => ошибка: # Невозможно получить доступ к элементу "FirstArgIsNotOuterAndPositionalOnly" для типа "_2"/"type[_2]" # Не удалось вызвать метод "__get__" для класса дескриптора "InnerClass[int, FirstArgIsNotOuterAndPositionalOnly, (foo: str, *, bar: float)]" Третий пример с аргументами, содержащими только ключевые слова, показывает, что средства проверки типов определили, что сигнатуры конфликтуют с Concatenate[], который не может представлять аргументы с ключевыми словами:
класс _3: @InnerClass # mypy => Аргумент 1 для «InnerClass» имеет несовместимый тип «type[FirstArgIsNotOuterAndKeywordOnly]»; ожидается "Callable[[Never, NamedArg(int, 'not_a'), NamedArg(str, 'foo'), NamedArg(float, 'bar')], FirstArgIsNotOuterAndKeywordOnly]" # пирайт => ошибка: # Аргумент типа "type[FirstArgIsNotOuterAndKeywordOnly]" нельзя присвоить параметру "inner_class" типа "(O@InnerClass, **P@InnerClass) -> I@InnerClass" в функции "__init__" # Тип "type[FirstArgIsNotOuterAndKeywordOnly]" нельзя назначить типу "(O@InnerClass, **P@InnerClass) -> I@InnerClass" класс FirstArgIsNotOuterAndKeywordOnly: def __init__(self, *, not_a: int, foo: str, bar: float) -> None: ... С другой стороны, результаты для вызовов reveal_type() были разными: сначала пирайт правильно определил, что произошла ошибка доступа к классу, но каким-то образом пропустил доступ к экземпляру.< /п> reveal_type(_3.FirstArgIsNotOuterAndKeywordOnly) show_type(_3().FirstArgIsNotOuterAndKeywordOnly) # mypy => (*, not_a: int, foo: str, bar: float) -> _3.FirstArgIsNotOuterAndKeywordOnly # пирайт => ошибка: # Тип «FirstArgIsNotOuterAndKeywordOnly» частично неизвестен. # (Неизвестно, foo: str, bar: float) -> FirstArgIsNotOuterAndKeywordOnly # => нет ошибки: # (foo: str, bar: float) -> FirstArgIsNotOuterAndKeywordOnly Последние три примера во многом аналогичны приведенным выше, за исключением того, что ожидаемые первые аргументы имеют правильный внешний тип. mypy по-прежнему не смог обнаружить правильные подписи, но Pyright был хорош (за исключением последнего примера, где он допустил ту же ошибку, что и в примере 3).
класс _4: @InnerClass # mypy + piright => хорошо класс FirstArgIsOuter: def __init__(self, c: _4, foo: str, *, bar: float) -> None: ... показ_типа (_4.FirstArgIsOuter) show_type(_4().FirstArgIsOuter) # mypy => (c: _4, foo: str, *, bar: float) -> _4.FirstArgIsOuter #pyright => (_4, foo: str, *, bar: float) -> FirstArgIsOuter # => (foo: str, *, bar: float) -> FirstArgIsOuter класс _5: @InnerClass класс FirstArgIsOuterAndPositionalOnly: def __init__(self, c: _5, /, foo: str, *, bar: float) -> Нет: ... показ_типа (_5.FirstArgIsOuterAndPositionalOnly) show_type(_5().FirstArgIsOuterAndPositionalOnly) # mypy => (_5, foo: str, *, bar: float) -> _5.FirstArgIsOuterAndPositionalOnly #pyright => (_5, foo: str, *, bar: float) -> FirstArgIsOuterAndPositionalOnly # => (foo: str, *, bar: float) -> FirstArgIsOuterAndPositionalOnly класс _6: @InnerClass # mypy + piright => ошибка класс FirstArgIsOuterAndKeywordOnly: def __init__(self, *, c: _6, foo: str, bar: float) -> None: ... показ_типа (_6.FirstArgIsOuterAndKeywordOnly) show_type(_6().FirstArgIsOuterAndKeywordOnly) # mypy => (*, c: _6, foo: str, bar: float) -> _6.FirstArgIsOuterAndKeywordOnly # пирайт => ошибка: # Тип «FirstArgIsOuterAndKeywordOnly» частично неизвестен. # (Неизвестно, foo: str, bar: float) -> FirstArgIsOuterAndKeywordOnly # => нет ошибки: # (foo: str, bar: float) -> FirstArgIsOuterAndKeywordOnly Вопрос Есть ли способ улучшить этот фрагмент кода с точки зрения подсказок типов? Я бы хотел, чтобы средства проверки типов выходили из строя раньше, как указано. Если это невозможно, то, по крайней мере, я хочу лучших результатов от mypy. Объяснение ошибки пирайта в примерах 3 и 6 было бы плюсом, но не обязательно.
Я использую Python 3.12 (со старым синтаксисом общего класса, поскольку mypy еще не поддерживает PEP 695) с флагами --strict как для mypy 1.8.0, так и дляpyright 1.1.344 , если это имеет значение.