将 Typing 和 Mypy 与描述符一起使用
Using Typing and Mypy with Descriptors
我查看了一些 SO 帖子和 github 与使用带描述符的打字相关的问题,但我无法解决我的问题。
我有包装器 类,我想将属性定义为可以获取和 "cast" 内部数据结构属性的描述。
class DataDescriptor(object):
def __init__(self, name: str, type_):
self.name = name
self.type_ = type_
def __get__(self, instance, cls):
if not instance:
raise AttributeError("this descriptor is for instances only")
value = getattr(instance._data, self.name)
return self.type_(value)
class City(object):
zip_code: str = DataDescriptor("zip_code", str)
# mypy: Incompatible types in assignment
population: float = DataDescriptor("population", float)
# mypy: Incompatible types in assignment
def __init__(self, data):
self._data = data
class InternalData:
# Will be consumed through city wrapper
def __init__(self):
self.zip_code = "12345-1234"
self.population = "12345"
self.population = "12345"
data = InternalData()
city = City(data)
assert city.zip_code == "12345-1234"
assert city.population == 12345.0
我想我也许可以使用 TypeVar,但我一直无法理解它。
这是我尝试过的——我想我可以动态描述描述符将采用"type",并且这种类型也是__get__
将return 的类型。我在正确的轨道上吗?
from typing import TypeVar, Type
T = TypeVar("T")
class DataDescriptor(object):
def __init__(self, name: str, type_: Type[T]):
self.name = name
self.type_ = type_
def __get__(self, instance, cls) -> T:
if not instance:
raise AttributeError("this descriptor is for instances only")
value = getattr(instance._data, self.name)
return self.type_(value)
# Too many arguments for "object"mypy(error)
您的解决方案很接近。为了让它充分发挥作用,您只需再进行三处更改:
使整个 DataDescriptor class 通用,而不仅仅是它的方法。
当您在构造函数和方法签名中单独使用 TypeVar 时,您最终要做的是使每个 方法 独立通用。这意味着绑定到 __init__
的 T 的任何值实际上最终将完全独立于 T __get__
的任何值 return!
这与您想要的完全相反:您希望不同方法之间的 T
值完全相同。
要修复,让 DataDescriptor 继承自 Generic[T]
。 (在运行时,这与继承自 object
基本相同。)
在 City 中,要么去掉两个字段的类型注释,要么将它们分别注释为 DataDescriptor[str]
和 DataDescriptor[float]
类型。
基本上,这里发生的事情是您的字段本身实际上是 DataDescriptor 对象,需要这样注释。稍后,当您实际尝试使用 city.zip_code
和 city.population
字段时,mypy 将意识到这些字段是描述符,并使它们的类型成为 __get__
的 return 类型方法是。
此行为对应于运行时发生的情况:您的属性 实际上是 描述符,只有当您尝试 访问时,您才会返回 float 或 str 那些属性。
在 DataDescriptor.__init__
的签名中,将 Type[T]
更改为 Callable[[str], T]
、Callable[[Any], T]
或 Callable[[...], T]
.
基本上,Type[T]
不起作用的原因是 mypy 不确切地知道您可能给描述符的是哪种 Type[...]
对象。例如,如果您尝试执行 foo = DataDescriptor('foo', object)
会发生什么?这将使 __get__
最终调用 object("some value")
,这会在运行时崩溃。
因此,让我们让您的 DataDescriptor 接受任何类型的转换器函数。根据你的需要,你可以让你的转换器函数只接受一个字符串(Callable[[str], T]
),任何任意类型的任何单个参数(Callable[[Any], T]
),或者任何数量的任意类型的参数( Callable[..., T]
).
将这些放在一起,您的最终示例将如下所示:
from typing import Generic, TypeVar, Any, Callable
T = TypeVar('T')
class DataDescriptor(Generic[T]):
# Note: I renamed `type_` to `converter` because I think that better
# reflects what this argument can now do.
def __init__(self, name: str, converter: Callable[[str], T]) -> None:
self.name = name
self.converter = converter
def __get__(self, instance: Any, cls: Any) -> T:
if not instance:
raise AttributeError("this descriptor is for instances only")
value = getattr(instance._data, self.name)
return self.converter(value)
class City(object):
# Note that 'str' and 'float' are still valid converters -- their
# constructors can both accept a single str argument.
#
# I also personally prefer omitting type hints on fields unless
# necessary: I think it looks cleaner that way.
zip_code = DataDescriptor("zip_code", str)
population = DataDescriptor("population", float)
def __init__(self, data):
self._data = data
class InternalData:
def __init__(self):
self.zip_code = "12345-1234"
self.population = "12345"
self.population = "12345"
data = InternalData()
city = City(data)
# reveal_type is a special pseudo-function that mypy understands:
# it'll make mypy print out the type of whatever expression you give it.
reveal_type(city.zip_code) # Revealed type is 'str'
reveal_type(city.population) # Revealed type is 'float'
我查看了一些 SO 帖子和 github 与使用带描述符的打字相关的问题,但我无法解决我的问题。
我有包装器 类,我想将属性定义为可以获取和 "cast" 内部数据结构属性的描述。
class DataDescriptor(object):
def __init__(self, name: str, type_):
self.name = name
self.type_ = type_
def __get__(self, instance, cls):
if not instance:
raise AttributeError("this descriptor is for instances only")
value = getattr(instance._data, self.name)
return self.type_(value)
class City(object):
zip_code: str = DataDescriptor("zip_code", str)
# mypy: Incompatible types in assignment
population: float = DataDescriptor("population", float)
# mypy: Incompatible types in assignment
def __init__(self, data):
self._data = data
class InternalData:
# Will be consumed through city wrapper
def __init__(self):
self.zip_code = "12345-1234"
self.population = "12345"
self.population = "12345"
data = InternalData()
city = City(data)
assert city.zip_code == "12345-1234"
assert city.population == 12345.0
我想我也许可以使用 TypeVar,但我一直无法理解它。
这是我尝试过的——我想我可以动态描述描述符将采用"type",并且这种类型也是__get__
将return 的类型。我在正确的轨道上吗?
from typing import TypeVar, Type
T = TypeVar("T")
class DataDescriptor(object):
def __init__(self, name: str, type_: Type[T]):
self.name = name
self.type_ = type_
def __get__(self, instance, cls) -> T:
if not instance:
raise AttributeError("this descriptor is for instances only")
value = getattr(instance._data, self.name)
return self.type_(value)
# Too many arguments for "object"mypy(error)
您的解决方案很接近。为了让它充分发挥作用,您只需再进行三处更改:
使整个 DataDescriptor class 通用,而不仅仅是它的方法。
当您在构造函数和方法签名中单独使用 TypeVar 时,您最终要做的是使每个 方法 独立通用。这意味着绑定到
__init__
的 T 的任何值实际上最终将完全独立于 T__get__
的任何值 return!这与您想要的完全相反:您希望不同方法之间的
T
值完全相同。要修复,让 DataDescriptor 继承自
Generic[T]
。 (在运行时,这与继承自object
基本相同。)在 City 中,要么去掉两个字段的类型注释,要么将它们分别注释为
DataDescriptor[str]
和DataDescriptor[float]
类型。基本上,这里发生的事情是您的字段本身实际上是 DataDescriptor 对象,需要这样注释。稍后,当您实际尝试使用
city.zip_code
和city.population
字段时,mypy 将意识到这些字段是描述符,并使它们的类型成为__get__
的 return 类型方法是。此行为对应于运行时发生的情况:您的属性 实际上是 描述符,只有当您尝试 访问时,您才会返回 float 或 str 那些属性。
在
DataDescriptor.__init__
的签名中,将Type[T]
更改为Callable[[str], T]
、Callable[[Any], T]
或Callable[[...], T]
.基本上,
Type[T]
不起作用的原因是 mypy 不确切地知道您可能给描述符的是哪种Type[...]
对象。例如,如果您尝试执行foo = DataDescriptor('foo', object)
会发生什么?这将使__get__
最终调用object("some value")
,这会在运行时崩溃。因此,让我们让您的 DataDescriptor 接受任何类型的转换器函数。根据你的需要,你可以让你的转换器函数只接受一个字符串(
Callable[[str], T]
),任何任意类型的任何单个参数(Callable[[Any], T]
),或者任何数量的任意类型的参数(Callable[..., T]
).
将这些放在一起,您的最终示例将如下所示:
from typing import Generic, TypeVar, Any, Callable
T = TypeVar('T')
class DataDescriptor(Generic[T]):
# Note: I renamed `type_` to `converter` because I think that better
# reflects what this argument can now do.
def __init__(self, name: str, converter: Callable[[str], T]) -> None:
self.name = name
self.converter = converter
def __get__(self, instance: Any, cls: Any) -> T:
if not instance:
raise AttributeError("this descriptor is for instances only")
value = getattr(instance._data, self.name)
return self.converter(value)
class City(object):
# Note that 'str' and 'float' are still valid converters -- their
# constructors can both accept a single str argument.
#
# I also personally prefer omitting type hints on fields unless
# necessary: I think it looks cleaner that way.
zip_code = DataDescriptor("zip_code", str)
population = DataDescriptor("population", float)
def __init__(self, data):
self._data = data
class InternalData:
def __init__(self):
self.zip_code = "12345-1234"
self.population = "12345"
self.population = "12345"
data = InternalData()
city = City(data)
# reveal_type is a special pseudo-function that mypy understands:
# it'll make mypy print out the type of whatever expression you give it.
reveal_type(city.zip_code) # Revealed type is 'str'
reveal_type(city.population) # Revealed type is 'float'