Python 类型检查协议和描述符
Python type-checking Protocols and 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)
class Person
是否隐式实现了 Named
协议? According to mypy, it isn't:
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]
只是两种不同的类型。很公平。
但是,对 first_name
使用普通的 str
或将其包装在获取和设置 str
的描述符中只是一个实现细节。此处的鸭式输入告诉我,我们将使用 first_name
(界面)的方式不会改变。
换句话说,Person
实现了 Named
。
附带说明一下,PyCharm 的类型检查器在这种特殊情况下不会报错(尽管我不确定这是故意的还是偶然的)。
根据typing.Protocol
的预期用途,我的理解有误吗?
我正在努力寻找它的参考,但我认为 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)
这个passes MyPy.
当涉及描述符时,我观察到关于 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)
class Person
是否隐式实现了 Named
协议? According to mypy, it isn't:
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]
只是两种不同的类型。很公平。
但是,对 first_name
使用普通的 str
或将其包装在获取和设置 str
的描述符中只是一个实现细节。此处的鸭式输入告诉我,我们将使用 first_name
(界面)的方式不会改变。
换句话说,Person
实现了 Named
。
附带说明一下,PyCharm 的类型检查器在这种特殊情况下不会报错(尽管我不确定这是故意的还是偶然的)。
根据typing.Protocol
的预期用途,我的理解有误吗?
我正在努力寻找它的参考,但我认为 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)
这个passes MyPy.