python 有没有一种方法可以在没有属性装饰器的情况下对类属性进行类型提示?

t5zmwmid  于 5个月前  发布在  Python
关注(0)|答案(3)|浏览(58)

考虑用Python编写一个接口类。接口将getter和setter方法隐藏在属性后面。有几个属性具有非常相似的结构,只是名称不同。为了减少代码重复,接口为其属性使用工厂方法:

from __future__ import annotations

class Interface:

    def property_factory(name: str) -> property:
        """Create a property depending on the name."""

        @property
        def _complex_property(self: Interface) -> str:
            # Do something complex with the provided name
            return name

        @_complex_property.setter
        def _complex_property(self: Interface, _: str):
            pass

        return _complex_property

    foo = property_factory("foo")  # Works just like an actual property
    bar = property_factory("bar")

def main():
    interface = Interface()
    interface.foo  # Is of type '(variable) foo: Any' instead of '(property) foo: str'

if __name__ == "__main__":
    main()

字符串
这个实现的一个问题是,Interface.fooInterface.bar将被标记为(variable) foo/bar: Any,即使它们应该是(property) foo/bar: str
使用内联类型提示,如

foo: str = property_factory("foo")


感觉有点误导,因为foo不是一个真正的字符串。
我并不完全坚持这种属性模式。如果有更好的方法来创建属性,我也很乐意改变这种模式。但是,请记住,接口需要属性,因为真实的代码在getter/setter方法中做更复杂的事情。此外,我希望保留属性,而不是像get_property(self,name:str)这样的通用方法。
我也可以分享我正在处理的实际代码,上面的例子只是我想到的一个最小的例子。

cvxl0en2

cvxl0en21#

疯狂的猜测:property_factory被类型提示返回property;修饰的方法/函数的类型丢失,这通常不会发生,因为类型检查器也会检查它是否在类的主体中使用。
propertyis not generic(yet)。然而,链接问题的OP本身提供了一个property的泛型子类作为可能的解决方案。那是将近4年前的事情了,当时类型系统并不像现在这样好,所以让我们重写这个类:

from typing import Any, Generic, TypeVar, overload, cast

T = TypeVar('T')  # The return type
I = TypeVar('I')  # The outer instance's type

class Property(property, Generic[I, T]):
    
    def __init__(
        self,
        fget: Callable[[I], T] | None = None,
        fset: Callable[[I, T], None] | None = None,
        fdel: Callable[[I], None] | None = None,
        doc: str | None = None
    ) -> None:
        super().__init__(fget, fset, fdel, doc)
    
    @overload
    def __get__(self, instance: None, owner: type[I] | None = None) -> Callable[[I], T]:
        ...
    
    @overload
    def __get__(self, instance: I, owner: type[I] | None = None) -> T:
        ...
    
    def __get__(self, instance: I | None, owner: type[I] | None = None) -> Callable[[I], T] | T:
        return cast(Callable[[I], T] | T, super().__get__(instance, owner))
    
    def __set__(self, instance: I, value: T) -> None:
        super().__set__(instance, value)
    
    def __delete__(self, instance: I) -> None:
        super().__delete__(instance)

字符串
现在我们有了一个通用的property类,原始的设计可以重写为:
(The原创作品也是,只需要使用-> Property[Interface, str]/return Property(_getter, _setter)

from collections.abc import Callable

Getter = Callable[['Interface'], str]
Setter = Callable[['Interface', str], None]

def complex_property(name: str) -> tuple[Getter, Setter]:
    def _getter(self: Interface) -> str:
        ...
    
    def _setter(self: Interface, value: str) -> None:
        ...
    
    return _getter, _setter


.然后可以用作:

class Interface:
    
    foo = Property(*complex_property("foo"))


假设bar是同一类的普通@property,下面是一些测试(mypy playgroundpyright playground):

instance = Interface()

reveal_type(Interface.foo)  # expected: Any/property/Callable
reveal_type(Interface.bar)  # expected: Any/property/Callable

reveal_type(instance.foo)   # expected: str
reveal_type(instance.bar)   # expected: str

instance.foo = 42           # expected: error
instance.bar = 43           # expected: error

instance.foo = 'lorem'      # expected: fine
instance.bar = 'ipsum'      # expected: fine

k10s72fa

k10s72fa2#

如果你不需要太多的特殊属性能力,你可以对任何变量、属性或属性使用类型提示,如下所示:

class MyClass:
    '''Description of my class'''

    my_property: float = 5.6

字符串
在某些编辑器中,包括VSCode,您也可以在变量、属性或属性之后添加文档字符串。
(This总是有效的Python,但不是所有的编辑器都知道将文档字符串与变量/属性/属性相关联。尝试一下,看看它是否能在编辑器中工作。

class MyClass:
    '''Description of my class'''

    my_property: float = 5.6
    '''Example description of `my_property`'''

    another_property: int = 4
    '''And another doc string here'''


下面是它在我的VSCode版本中的样子:x1c 0d1x

balp4ylt

balp4ylt3#

在您当前的实现中,出现此问题是因为类型提示系统无法将property_factory创建的属性识别为具有特定类型的属性。解决此问题的一种方法是使用typing.cast函数将属性显式转换为所需类型。这样,您可以提供更准确的类型提示。
以下是如何修改代码:

from __future__ import annotations
from typing import cast

class Interface:

    @staticmethod
    def property_factory(name: str) -> property:
        """Create a property depending on the name."""

        @property
        def _complex_property(self: Interface) -> str:
            # Do something complex with the provided name
            return name

        @_complex_property.setter
        def _complex_property(self: Interface, _: str):
            pass

        return cast(property, _complex_property)

    foo: str = property_factory("foo")
    bar: str = property_factory("bar")

def main():
    interface = Interface()
    value: str = interface.foo  # Now has the correct type hint

if __name__ == "__main__":
    main()

字符串
在此修改中,我添加了cast(property,_complex_property)行,以显式地将创建的属性转换为属性类型。这允许您将foo和bar的类型提示指定为str。
虽然这解决了类型提示问题,但值得注意的是,使用强制转换本质上是告诉类型检查器信任您的Assert。因此,请确保实际实现符合指定的类型,以避免潜在的运行时错误。

相关问题