如何为Python魔术方法`__get__`提供类型提示

huangapple go评论78阅读模式
英文:

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

你需要overload __get__ 方法。

我自己不使用VSCode,但我使用MyPy测试了下面的代码,并且我希望Pyright也能正确地推断类型。

Python >=3.9

为了使这个尽可能灵活,我建议将Foo变为泛型,涵盖以下内容:

  1. 使用描述符/装饰器的类,
  2. 被装饰方法的参数规范,以及
  3. 被装饰方法的返回类型。
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 &gt;=3.10,你可以直接从typing中导入ConcatenateParamSpec,并且可以使用|符号代替typing.Union
  • 如果你使用Python &gt;=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 &gt;=3.9

To make this as flexible as possible, I would suggest making Foo generic in terms of

  1. the class using the descriptor/decorator,
  2. the parameter specification of the decorated method, and
  3. 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(&quot;T&quot;)    # class using the descriptor
P = ParamSpec(&quot;P&quot;)  # parameter specs of the decorated method
R = TypeVar(&quot;R&quot;)    # 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]) -&gt; None:
        self.method = method

    @overload
    def __get__(self, instance: T, owner: object) -&gt; R: ...

    @overload
    def __get__(self, instance: None, owner: object) -&gt; Self: ...

    def __get__(self, instance: Union[T, None], owner: object) -&gt; 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) -&gt; int:
        return 1


a = Bar().do_something
b = Bar.do_something

print(type(a), type(b))  # &lt;class &#39;int&#39;&gt; &lt;class &#39;__main__.Foo&#39;&gt;
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 &gt;=3.10, you can import Concatenate and ParamSpec directly from typing and you can use the |-notation instead of typing.Union.
  • If you are on Python &gt;=3.11 you can import Self directly from typing as well, meaning you won't need typing_extensions at all.

Python &lt;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(&quot;R&quot;)    # return value of the decorated method


class Foo(Generic[R]):
    method: Callable[..., R]

    def __init__(self, method: Callable[..., R]) -&gt; None:
        self.method = method

    @overload
    def __get__(self, instance: None, owner: object) -&gt; Foo[R]: ...

    @overload
    def __get__(self, instance: object, owner: object) -&gt; R: ...

    def __get__(self, instance: object, owner: object) -&gt; 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]

huangapple
  • 本文由 发表于 2023年3月31日 17:47:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/75897075.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定