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

Python 将键入和Mypy与描述符一起使用,python,mypy,python-typing,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):

我已经看过了一些关于使用描述符打字的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,但我还没能完全理解它

这就是我尝试过的——我想我可以动态地描述描述符将采用“类型”,并且该类型也是
\uuuuu get\uuuu
将返回的类型。我走对了吗

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类通用,而不仅仅是其方法

    当您在构造函数中单独使用TypeVar和方法签名时,您最终要做的是使每个方法独立地通用。这意味着无论绑定到
    \uuuuu init\uuuu
    的T的值是什么,它实际上都将完全独立于T
    \uuu get\uuuu
    返回的值

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

    要修复此问题,请让DataDescriptor继承自
    Generic[T]
    。(在运行时,这与从
    对象
    继承基本相同)

  • 在City中,要么去掉两个字段的类型注释,要么将它们分别注释为
    DataDescriptor[str]
    DataDescriptor[float]
    类型

    基本上,这里发生的事情是,字段本身实际上是DataDescriptor对象,需要进行注释。稍后,当您实际尝试使用
    city.zip\u code
    city.population
    字段时,mypy将意识到这些字段是描述符,并使它们的类型与
    \u get\uuu
    方法的返回类型不同

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

  • 数据描述符的签名中,将
    类型[T]
    更改为
    可调用[[str],T]
    可调用[[Any],T]
    ,或
    可调用[…],T]

    基本上,做
    Type[T]
    不起作用的原因是mypy不知道您可能会给描述符指定哪种类型的
    Type[…]
    对象。例如,如果您尝试执行
    foo=DataDescriptor('foo',object)
    ,会发生什么?这将使
    \uuu get\uuu
    最终调用
    对象(“某个值”)
    ,这将在运行时崩溃

    因此,让您的DataDescriptor接受任何类型的转换器函数。根据需要,您可以让转换器函数只接受一个字符串(
    Callable[[str],T]
    ),任意类型的任何单个参数(
    Callable[[any],T]
    ),或者任意类型的任意数量的参数(
    Callable[…,T]

  • 将这些放在一起,您的最后一个示例如下所示:

    输入import Generic、TypeVar、Any、Callable
    T=TypeVar('T')
    类数据描述符(泛型[T]):
    #注意:我将'type_uu`重命名为'converter',因为我觉得这样更好
    #反映了这个论点现在可以做什么。
    def uu init uuu(self,name:str,converter:Callable[[str],T])->无:
    self.name=名称
    self.converter=转换器
    def uu get uu(self,instance:Any,cls:Any)->T:
    如果没有,请举例说明:
    raise AttributeError(“此描述符仅用于实例”)
    value=getattr(实例。\u数据,self.name)
    返回自转换器(值)
    类别城市(对象):
    #注意,“str”和“float”仍然是有效的转换器——它们的
    #构造函数都可以接受单个str参数。
    #
    #我个人也喜欢省略字段上的类型提示,除非
    #必要:我觉得那样看起来更干净。
    邮政编码=数据描述符(“邮政编码”,str)
    填充=数据描述符(“填充”,浮点)
    定义初始化(自身,数据):
    self.\u data=数据
    类内部数据:
    定义初始化(自):
    self.zip_code=“12345-1234”
    self.population=“12345”
    self.population=“12345”
    数据=内部数据()
    城市=城市(数据)
    #reveal_type是mypy理解的一个特殊伪函数:
    #它会让mypy打印出你给它的任何表达式的类型。
    显示类型(city.zip_code)#显示类型为'str'
    显示类型(城市人口)#显示类型为“浮动”
    
    感谢您的全面和周到的回复-正如所述。