返回实例的classmethod的类型注释

99

我应该如何为返回cls实例的@classmethod添加注释?以下是一个不好的示例:

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

    @classmethod
    def with_stuff_appended(cls, bar: str) -> ???:
        return cls(bar + "stuff")

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


3
“‘Foo’是正确的,这就是你可以(或应该)强制执行的返回值。" - jonrsharpe
1
https://mypy.readthedocs.io/en/latest/more_types.html#precise-typing-of-alternative-constructors - Andrew
2
由于Python 3.11通过PEP-0673引入了typing.Self,如果不需要向后兼容,则可以参考此答案中的第二个示例(即使用-> Self:)。 - metatoaster
3个回答

120
关键在于要为cls参数明确添加注释,结合泛型TypeVarType,来表示一个类而不是实例本身,如下所示:
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())

通常情况下,您可以不注释cls(和self),但如果需要引用特定的子类,可以添加显式注释。请注意,此功能仍处于实验阶段,在某些情况下可能存在错误。您还可能需要使用从Github克隆的最新版本的mypy,而不是pypi上提供的版本——我不记得该版本是否支持classmethods的这个特性。

@taway -- 哇,太酷了!我本来以为不行,但很高兴听到我错了! - Michael0x2a
在我看来,带有类型检查的Python可以与Rust在复杂性和上市时间方面进行比较。但是mypy已经多次为我节省了时间,因此它绝对是必备工具。 - Fedorov7890
1
@BarneySzabolcs 类型注释是可选的。 - Andrew
4
好的回答!如果被接受的话,可能值得提到PEP 673 - Neil G
你知道如果Parent是一个泛型类,这个是如何工作的吗?https://dev59.com/j8Tra4cB1Zd3GeqP7nGz - jakun
显示剩余2条评论

65

仅为完整性,在Python 3.7中,您可以使用PEP 563中定义的延迟评估注释,只需在文件开头导入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")

根据文档,从Python 3.11开始,这个导入将会自动生效。


11
对我来说,这似乎是个令人不快的惊喜。如果我说“-> Foo”,那么我希望这个函数返回一个类型为“Foo”的对象,而不是一个子类。 - Sebastian Wagner
8
@SebastianWagner,我对评论有点迷惑,我的答案只涵盖如何使当前Python中的注释更好,而没有涉及到其他内容。 - Ignacio Vergara Kausel
13
如果你对Foo进行子类化,那么classmethod将返回子类,而不是Foo,但类型定义中却写着Foo。 - Maiku Mori
11
对我来说,这看起来非常自然。 子类的一个实例也是该类的实例。 你可以使用“isinstance”来进行实验。 这对应于面向对象课程中教授的基础知识,即您可以使用父类引用来引用子类实例。 - xuhdev
9
这不是对原帖问题的回答。如果某个方法仅存在于子类而不是父类中,则每次都需要返回正确的类型。我相信很多人知道如何使用延迟注释,仍然会进入这个讨论串并从@Michael0x2a的回答中受益(我的情况就是这样)。 - gcharbon
显示剩余3条评论

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

The Self type annotation is also useful for classmethods that return an instance of the class that they operate on. For example, from_config in the following snippet builds a Shape object from a given config [...]

from typing import Self

class Shape:
    @classmethod
    def from_config(cls, config: dict[str, float]) -> Self:
        return cls(config["scale"])
这个正确处理了子类化,正如问题所要求的那样。
class Circle(Shape):
    pass

Circle.from_config(config)  # mypy says this is a Circle

这里cls仍然具有其类型推断 - 如果我想要这个,我会写cls: type[Self],但是然后mypy会抱怨:Method cannot have explicit self annotation and Self type。这是mypy的不足还是有什么问题吗?快速编辑:刚意识到Mypy对cls: type[Self]有问题,但对cls: Type[Self]没有问题。有趣。 - jonaslb
@jonaslb 使用cls: typing.Type[Self]而不是cls是与mypy兼容的,但自3.9版本起已被弃用。我不确定为什么mypy会抱怨。我也不确定为什么您想要注释cls而不是类方法的返回值。 - TrakJohnson
1
哦,我不会摒弃返回类型。这只涉及到对cls参数的正确注解。我知道Mypy可以推断它,所以严格来说不必要加上它,但我认为将其包括进去是一个相关的观点。 - jonaslb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接