英文:
How to type hint python magic __get__ method
问题
如何正确地为__get__和method进行类型提示,以使Pylance理解Bar().do_something的返回类型与do_something的返回类型相同?(就像标准的property一样)
from typing import Type, TypeVar
T = TypeVar('T')
class Foo:
   def __init__(self, method: Type[T]) -> None:
       self.method = method
   def __get__(self, instance, owner) -> Type[T]:
       if instance is None:
          return self
       return self.method(instance)
class Bar:
    @Foo
    def do_something(self) -> int:
        return 1
英文:
Suppose we have the following classes:
class Foo:
   def __init__(self, method):
       self.method = method
   def __get__(self, instance, owner):
       if instance is None:
          return self
       return self.method(instance)
class Bar:
    @Foo
    def do_something(self) -> int:
        return 1
Bar().do_something  # is 1
Bar.do_something    # is Foo object
How to type hint __get__ and method correctly so that Pylance understands Bar().do_something is of the return type of do_something? (like a standard property)
答案1
得分: 2
我自己不使用VSCode,但我使用MyPy测试了下面的代码,并且我希望Pyright也能正确地推断类型。
Python >=3.9
为了使这个尽可能灵活,我建议将Foo变为泛型,涵盖以下内容:
- 使用描述符/装饰器的类,
 - 被装饰方法的参数规范,以及
 - 被装饰方法的返回类型。
 
from collections.abc import Callable
from typing import Generic, TypeVar, Union, overload
from typing_extensions import Concatenate, ParamSpec, Self
T = TypeVar("T")    # 使用描述符的类
P = ParamSpec("P")  # 装饰的方法的参数规范
R = TypeVar("R")    # 装饰的方法的返回值
class Foo(Generic[T, P, R]):
    method: Callable[Concatenate[T, P], R]
    def __init__(self, method: Callable[Concatenate[T, P], R]) -> None:
        self.method = method
    @overload
    def __get__(self, instance: T, owner: object) -> R: ...
    @overload
    def __get(self, instance: None, owner: object) -> Self: ...
    def __get__(self, instance: Union[T, None], owner: object) -> Union[Self, R]:
        if instance is None:
            return self
        return self.method(instance)
演示:
from typing import TYPE_CHECKING
class Bar:
    @Foo
    def do_something(self) -> int:
        return 1
a = Bar().do_something
b = Bar.do_something
print(type(a), type(b))  # <class 'int'> <class '__main__.Foo'>
if TYPE_CHECKING:
    reveal_locals()
对这个代码运行MyPy会得到期望的输出:
note: Revealed local types are:
note:     a: builtins.int
note:     b: Foo[Bar, [], builtins.int]
注意:(感谢@SUTerliakov指出的一些内容)
- 如果你使用Python 
>=3.10,你可以直接从typing中导入Concatenate和ParamSpec,并且可以使用|符号代替typing.Union。 - 如果你使用Python 
>=3.11,你也可以直接从typing中导入Self,这意味着你不需要typing_extensions。 
英文:
You'll need to overload the __get__ method.
I do not use VSCode myself, but I tested the code below with MyPy and I would expect Pyright to infer the types correctly as well.
Python >=3.9
To make this as flexible as possible, I would suggest making Foo generic in terms of
- the class using the descriptor/decorator,
 - the parameter specification of the decorated method, and
 - the return type of the decorated method.
 
from collections.abc import Callable
from typing import Generic, TypeVar, Union, overload
from typing_extensions import Concatenate, ParamSpec, Self
T = TypeVar("T")    # class using the descriptor
P = ParamSpec("P")  # parameter specs of the decorated method
R = TypeVar("R")    # return value of the decorated method
class Foo(Generic[T, P, R]):
    method: Callable[Concatenate[T, P], R]
    def __init__(self, method: Callable[Concatenate[T, P], R]) -> None:
        self.method = method
    @overload
    def __get__(self, instance: T, owner: object) -> R: ...
    @overload
    def __get__(self, instance: None, owner: object) -> Self: ...
    def __get__(self, instance: Union[T, None], owner: object) -> Union[Self, R]:
        if instance is None:
            return self
        return self.method(instance)
Demo:
from typing import TYPE_CHECKING
class Bar:
    @Foo
    def do_something(self) -> int:
        return 1
a = Bar().do_something
b = Bar.do_something
print(type(a), type(b))  # <class 'int'> <class '__main__.Foo'>
if TYPE_CHECKING:
    reveal_locals()
Running MyPy over this gives the desired output:
note: Revealed local types are:
note:     a: builtins.int
note:     b: Foo[Bar, [], builtins.int]
NOTE: (thanks to @SUTerliakov for pointing some of this out)
- If you are on Python 
>=3.10, you can importConcatenateandParamSpecdirectly fromtypingand you can use the|-notation instead oftyping.Union. - If you are on Python 
>=3.11you can importSelfdirectly fromtypingas well, meaning you won't needtyping_extensionsat all. 
Python <3.9
Without Concatenate, ParamSpec and Self we can still make Foo generic in terms of the return value of the decorated method:
from __future__ import annotations
from collections.abc import Callable
from typing import Generic, TypeVar, Union, overload
R = TypeVar("R")    # return value of the decorated method
class Foo(Generic[R]):
    method: Callable[..., R]
    def __init__(self, method: Callable[..., R]) -> None:
        self.method = method
    @overload
    def __get__(self, instance: None, owner: object) -> Foo[R]: ...
    @overload
    def __get__(self, instance: object, owner: object) -> R: ...
    def __get__(self, instance: object, owner: object) -> Union[Foo[R], R]:
        if instance is None:
            return self
        return self.method(instance)
MyPy output for the same demo script from above:
note: Revealed local types are:
note:     a: builtins.int
note:     b: Foo[builtins.int]
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论