Протоколы и дескрипторы проверки типов Python

#python #type-hinting #mypy #python-typing #python-descriptors

Вопрос:

Я наблюдаю поведение, typing.Protocol когда задействованы дескрипторы, которое я не совсем полностью понимаю. Рассмотрим следующий код:

 import typing as t

T = t.TypeVar('T')


class MyDescriptor(t.Generic[T]):

    def __set_name__(self, owner, name):
        self.name = name

    def __set__(self, instance, value: T):
        instance.__dict__[self.name] = value

    def __get__(self, instance, owner) -> T:
        return instance.__dict__[self.name]


class Named(t.Protocol):

    first_name: str


class Person:

    first_name = MyDescriptor[str]()
    age: int

    def __init__(self):
        self.first_name = 'John'


def greet(obj: Named):
    print(f'Hello {obj.first_name}')


person = Person()
greet(person)
 

Является ли класс Person неявно реализующим Named протокол? По словам майпи, это не так:

 error: Argument 1 to "greet" has incompatible type "Person"; expected "Named"
note: Following member(s) of "Person" have conflicts:
note:     first_name: expected "str", got "MyDescriptor[str]"
 

Я думаю, это потому, что mypy быстро приходит к такому выводу str и MyDescriptor[str] просто состоит из 2 разных типов. Справедливо.

Тем не менее, использование простого str для first_name или обертывание его в дескриптор, который получает и задает a str , — это всего лишь деталь реализации. Ввод утки здесь говорит мне, что способ, которым мы будем использовать first_name (интерфейс), не изменится.

Другими словами, Person орудия Named .

В качестве примечания, проверка типов PyCharm в данном конкретном случае не жалуется (хотя я не уверен, сделано ли это специально или случайно).

В соответствии с предполагаемым использованием typing.Protocol , мое понимание неверно?

Ответ №1:

Я изо всех сил пытаюсь найти ссылку на это, но я думаю, что MyPy немного борется с некоторыми более тонкими деталями дескрипторов (вы можете понять, почему там происходит изрядная часть магии). Я думаю, что обходным путем здесь было бы просто использовать typing.cast :

 import typing as t

T = t.TypeVar('T')


class MyDescriptor(t.Generic[T]):
    def __set_name__(self, owner, name: str) -> None:
        self.name = name

    def __set__(self, instance, value: T) -> None:
        instance.__dict__[self.name] = value

    def __get__(self, instance, owner) -> T:
        name = instance.__dict__[self.name]
        return t.cast(T, name)


class Named(t.Protocol):
    first_name: str


class Person:
    first_name = t.cast(str, MyDescriptor[str]())
    age: int

    def __init__(self) -> None:
        self.first_name = 'John'


def greet(obj: Named) -> None:
    print(f'Hello {obj.first_name}')


person = Person()
greet(person)
 

Это проходит мимо моего внимания.