对描述符使用键入和Mypy

时间:2019-08-07 18:38:11

标签: python mypy python-typing

我看了一些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 个答案:

答案 0 :(得分:2)

您的解决方案即将结束。为了使其完全正常工作,您只需要再进行三处更改:

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

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

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

    要修复,请让DataDescriptor从Generic[T]继承。 (在运行时,这与从object继承是相同的。)

  2. 在City范围内,要么摆脱两个字段的类型注释,要么分别将其注释为DataDescriptor[str]DataDescriptor[float]类型。

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

    此行为与运行时发生的情况相对应:您的属性实际上是 描述符,并且只有在尝试访问这些属性时,您才能返回float或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'