类方法返回实例的MyPy注释

taw*_*way 12 python mypy

我该如何注释一个@classmethod返回实例的cls?这是一个糟糕的例子:

class Foo(object):
    def __init__(self, bar: str):
        self.bar = bar

    @classmethod
    def with_stuff_appended(cls, bar: str) -> ???:
        return cls(bar + "stuff")
Run Code Online (Sandbox Code Playgroud)

这会返回一个Foo但更准确地返回Foo调用此子类的任何子类,因此使用注释-> "Foo"不够好.

Mic*_*x2a 22

诀窍是注释明确添加到cls参数,结合TypeVar,为仿制药,并Type代表了一类而不是实例本身,就像这样:

from typing import TypeVar, Type

# Create a generic variable that can be 'Parent', or any subclass.
T = TypeVar('T', bound='Parent')

class Parent:
    def __init__(self, bar: str) -> None:
        self.bar = bar

    @classmethod
    def with_stuff_appended(cls: Type[T], bar: str) -> T:
        # We annotate 'cls' with a typevar so that we can
        # type our return type more precisely
        return cls(bar + "stuff")

class Child(Parent):
    # If you're going to redefine __init__, make sure it
    # has a signature that's compatible with the Parent's __init__,
    # since mypy currently doesn't check for that.

    def child_only(self) -> int:
        return 3

# Mypy correctly infers that p is of type 'Parent',
# and c is of type 'Child'.
p = Parent.with_stuff_appended("10")
c = Child.with_stuff_appended("20")

# We can verify this ourself by using the special 'reveal_type'
# function. Be sure to delete these lines before running your
# code -- this function is something only mypy understands
# (it's meant to help with debugging your types).
reveal_type(p)  # Revealed type is 'test.Parent*'
reveal_type(c)  # Revealed type is 'test.Child*'

# So, these all typecheck
print(p.bar)
print(c.bar)
print(c.child_only())
Run Code Online (Sandbox Code Playgroud)

通常,您可以保留cls(和self)未注释,但是如果需要引用特定的子类,则可以添加显式注释.请注意,此功能仍处于试验阶段,在某些情况下可能存在问题.您可能还需要使用从Github克隆的最新版mypy,而不是pypi上可用的内容 - 我不记得该版本是否支持classmethods的此功能.

  • 这看起来很可怕。Python 是新的 C++98 吗?:''''( (7认同)
  • 很好的答案!当它被接受时,可能值得一提[PEP 673](https://www.python.org/dev/peps/pep-0673/)。 (4认同)

Tra*_*son 8

Python 版本 >= 3.11:从 Python 3.11 开始,您现在可以使用typing.Self来避免声明TypeVar. 它可以与PEP 673 中指定的@classmethod一起使用:

类型Self注释对于返回其操作的类的实例的类方法也很有用。例如,from_config在下面的代码片段中,Shape从给定的配置构建一个对象[...]

from typing import Self

class Shape:
    @classmethod
    def from_config(cls, config: dict[str, float]) -> Self:
        return cls(config["scale"])
Run Code Online (Sandbox Code Playgroud)

根据问题的要求,这可以正确处理子类化:

class Circle(Shape):
    pass

Circle.from_config(config)  # mypy says this is a Circle
Run Code Online (Sandbox Code Playgroud)


Ign*_*sel 6

仅出于完整性考虑,在Python 3.7中,您可以通过在文件的开头进行导入来使用PEP563中postponed evaluation of annotations定义的。from __future__ import annotations

然后对于您的代码,它看起来像

from __future__ import annotations

class Foo(object):
    def __init__(self, bar: str):
        self.bar = bar

    @classmethod
    def with_stuff_appended(cls, bar: str) -> Foo:
        return cls(bar + "stuff")
Run Code Online (Sandbox Code Playgroud)

  • 这不是OP问题的答案。如果某些方法仅存在于子类而不是父类中,则每次都需要返回正确的类型。我确信很多人知道如何使用推迟注释,但仍然在这个线程上结束,并从 @Michael0x2a 答案中受益(这就是我的情况) (9认同)
  • 对我来说这似乎是一个令人讨厌的惊喜。如果我说 `-> Foo`,那么我期望这个函数返回一个 `Foo` 类型的对象,而不是子类。 (5认同)
  • 因为如果你子类化 Foo,那么类方法将返回子类,而不是 Foo,但类型定义显示 Foo。 (4认同)
  • @SebastianWagner我对评论有些迷惑,我的回答并没有解决这个问题,而只是解决了如何使当前Python中的注释更好。 (3认同)
  • @MaikuMori 这对我来说看起来很自然。子类的实例也是该类的实例。您可以使用“isinstance”进行实验。这对应于 OO 类中教授的基本内容,您可以使用父类引用来引用子类实例。 (3认同)
  • @SebastianWagner 这并不奇怪。为什么你会因为想到子类而感到困惑? (2认同)