将键入和 Mypy 与描述符一起使用



我已经查看了一些与使用带有描述符的打字相关的SO帖子和github问题,但我无法解决我的问题。

我有包装类,我想将属性定义为可以获取和"转换"内部数据结构属性的描述符。

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,但我无法理解它。

这就是我尝试过的 - 我想我可以动态描述描述符将采用"类型",并且此类型也是__get__将返回的类型。我走在正确的轨道上吗?

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)

您的解决方案很接近。为了使它完全正常工作,您只需再进行三项更改:

  1. 使整个 DataDescriptor 类泛型,而不仅仅是其方法。

    当您在构造函数和方法签名中使用 TypeVar 本身时,您最终要做的是使每个方法独立泛型。这意味着任何绑定到__init__的 T 的值实际上最终将完全独立于 T 的任何值__get__将返回!

    这与你想要的完全相反:你希望不同方法之间的T值完全相同。

    要修复,请让数据描述符继承自Generic[T]。(在运行时,这通常与从object继承相同。

  2. 在 City 中,删除两个字段的类型注释,或将它们分别注释为类型DataDescriptor[str]DataDescriptor[float]

    基本上,这里发生的事情是你的字段本身实际上是 DataDescriptor 对象,需要这样注释。稍后,当您实际尝试使用city.zip_codecity.population字段时,mypy 将意识到这些字段是描述符,并使它们的类型成为您的__get__方法的返回类型。

    此行为对应于运行时发生的情况:您的属性实际上是描述符,并且只有在尝试访问这些属性时才会返回浮点数或 str。

  3. 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'

最新更新