我可以在 python 中将参数类型提示为类本身的泛型类型吗? [复制]

原文标题Can I typehint a parameter as a generic type of the class itself in python? [duplicate]

我在 Python 3 中有以下代码:

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: Position) -> Position:
        return Position(self.x + other.x, self.y + other.y)

但是我的编辑器(PyCharm)说引用Position无法解析(在__add__方法中)。我应该如何指定我期望返回类型是类型Position

编辑:我认为这实际上是一个 PyCharm 问题。它实际上使用了警告和代码完成中的信息。

但是,如果我错了,请纠正我,并且需要使用其他语法。

原文链接:https://stackoverflow.com//questions/71918857/can-i-typehint-a-parameter-as-a-generic-type-of-the-class-itself-in-python

回复

我来回复
  • Paulo Scardine的头像
    Paulo Scardine 评论

    TL;DR:从今天(2019 年)开始,在 Python 3.7+ 中,您必须使用“未来”语句打开此功能,from __future__ import annotations

    from __future__ import annotations启用的行为可能会成为未来Python版本的默认设置,并且将在Python 3.10中成为默认设置。但是,3.10中的更改在最后一刻被恢复,现在可能根本不会发生。)

    在 Python 3.6 或更低版本中,您应该使用字符串。


    我猜你有这个例外:

    NameError: name 'Position' is not defined
    

    这是因为Position必须先定义,然后才能在注释中使用,除非您使用启用了 PEP 563changes 的 Python。

    Python 3.7+:from __future__ import annotations

    Python 3.7 引入了PEP 563:对注解的延迟评估。使用future 语句的模块from __future__ import annotations会自动将注解存储为字符串:

    from __future__ import annotations
    
    class Position:
        def __add__(self, other: Position) -> Position:
            ...
    

    这已计划成为 Python 3.10 中的默认设置,但现在这一更改已被推迟。由于 Python 仍然是一种动态类型语言,因此在运行时不进行类型检查,键入注解应该不会影响性能,对吧?错误的!在 Python 3.7 之前,typing 模块曾经是核心中最慢的 Python 模块之一,对于涉及导入typing模块的代码,升级到 3.7 时,您将看到性能提升多达 7 倍。

    Python <3.7:使用字符串

    根据 PEP 484,您应该使用字符串而不是类本身:

    class Position:
        ...
        def __add__(self, other: 'Position') -> 'Position':
           ...
    

    如果您使用 Django 框架,这可能很熟悉,因为 Django 模型也使用字符串进行前向引用(外来模型所在的外键定义self或尚未声明)。这应该适用于 Pycharm 和其他工具。

    来源

    PEP 484 和 PEP 563 的相关部分,为您省去旅行:

    前向引用

    当类型提示包含尚未定义的名称时,该定义可以表示为字符串文字,稍后再解析。

    这种情况经常发生的情况是定义容器类,其中被定义的类出现在某些方法的签名中。例如,以下代码(简单二叉树实现的开始)不起作用:

    class Tree:
        def __init__(self, left: Tree, right: Tree):
            self.left = left
            self.right = right
    

    为了解决这个问题,我们写道:

    class Tree:
        def __init__(self, left: 'Tree', right: 'Tree'):
            self.left = left
            self.right = right
    

    字符串字面量应该包含一个有效的 Python 表达式(即 compile(lit, ”, ‘eval’) 应该是一个有效的代码对象)并且在模块完全加载后它应该没有错误地计算。本地和全局命名空间在它被评估的应该是相同的命名空间,相同函数的默认参数将被评估。

    和 PEP 563:

    执行

    在 Python 3.10 中,函数和变量注解将不再在定义时进行求值。相反,字符串形式将保留在各自的__annotations__字典中。静态类型检查器将看不到行为差异,而在运行时使用注解的工具将不得不进行延期评估。

    在 Python 3.7 中启用未来行为

    从 Python 3.7 开始,可以使用以下特殊导入启用上述功能:

    from __future__ import annotations
    

    你可能想做的事情

    A. 定义一个假人Position

    在类定义之前,放置一个虚拟定义:

    class Position(object):
        pass
    
    
    class Position(object):
        ...
    

    这将摆脱NameError,甚至看起来还可以:

    >>> Position.__add__.__annotations__
    {'other': __main__.Position, 'return': __main__.Position}
    

    但是是吗?

    >>> for k, v in Position.__add__.__annotations__.items():
    ...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
    return is Position: False
    other is Position: False
    

    B. Monkey-patch 为了添加注释:

    您可能想尝试一些 Python 元编程魔法并编写一个装饰器来猴子修补类定义以添加注释:

    class Position:
        ...
        def __add__(self, other):
            return self.__class__(self.x + other.x, self.y + other.y)
    

    装饰者应该负责相当于:

    Position.__add__.__annotations__['return'] = Position
    Position.__add__.__annotations__['other'] = Position
    

    至少看起来是对的:

    >>> for k, v in Position.__add__.__annotations__.items():
    ...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
    return is Position: True
    other is Position: True
    

    大概是太麻烦了。

    2年前 0条评论
  • 从 Python 3.11(将于 2022 年底发布)开始,您将能够使用Self作为返回类型。

    from typing import Self
    
    
    class Position:
    
        def __init__(self, x: int, y: int):
            self.x = x
            self.y = y
    
        def __add__(self, other: Self) -> Self:
            return Position(self.x + other.x, self.y + other.y)
    
    2年前 0条评论
  • vbraun的头像
    vbraun 评论

    将类型指定为字符串很好,但总是让我有点恼火,因为我们基本上是在绕过解析器。所以你最好不要拼错这些文字字符串中的任何一个:

    def __add__(self, other: 'Position') -> 'Position':
        return Position(self.x + other.x, self.y + other.y)
    

    一个细微的变化是使用绑定的类型变量,至少在声明类型变量时您只需编写一次字符串:

    from typing import TypeVar
    
    T = TypeVar('T', bound='Position')
    
    class Position:
    
        def __init__(self, x: int, y: int):
            self.x = x
            self.y = y
    
        def __add__(self, other: T) -> T:
            return Position(self.x + other.x, self.y + other.y)
    
    2年前 0条评论
  • MacFreek的头像
    MacFreek 评论

    如果您只关心修复NameError: name 'Position' is not defined,您可以将类名指定为字符串:

    def __add__(self, other: 'Position') -> 'Position':
    

    或者,如果您使用 Python 3.7 或更高版本,请将以下行添加到代码顶部(就在其他导入之前)

    from __future__ import annotations
    

    但是,如果您还希望它适用于子类并返回特定的子类,则需要使用 aTypeVar 将该方法注释为非泛型方法。

    稍微不常见的是TypeVar绑定到self的类型。基本上,这个类型提示告诉类型检查器__add__()copy()的返回类型与self的类型相同。

    from __future__ import annotations
    
    from typing import TypeVar
    
    T = TypeVar('T', bound=Position)
    
    class Position:
        
        def __init__(self, x: int, y: int):
            self.x = x
            self.y = y
        
        def __add__(self: T, other: Position) -> T:
            return type(self)(self.x + other.x, self.y + other.y)
        
        def copy(self: T) -> T:
            return type(self)(self.x, self.y)
    
    2年前 0条评论
  • jsbueno的头像
    jsbueno 评论

    在解析类主体本身时,名称“Position”不可用。我不知道你是如何使用类型声明的,但是 Python 的 PEP 484 – 如果使用这些输入提示说你可以简单地将名称作为字符串放在这一点上,这是大多数模式应该使用的:

    def __add__(self, other: 'Position') -> 'Position':
        return Position(self.x + other.x, self.y + other.y)
    

    检查前向引用的 PEP 484 部分 – 符合该标准的工具将知道从那里解开类名并使用它。 (始终重要的是要记住 Python 语言本身对这些注释没有任何作用。它们通常用于静态代码分析,或者可以有一个库/框架用于在运行时进行类型检查 – 但您必须明确设置。)

    更新:另外,从 Python 3.7 开始,请查看 PEP 563。从 Python 3.8 开始,可以编写from __future__ import annotations来推迟对注释的评估。前向引用类应该可以直接工作。

    2年前 0条评论
  • Yvon DUTAPIS的头像
    Yvon DUTAPIS 评论

    当基于字符串的类型提示可以接受时,也可以使用__qualname__项。它包含类的名称,并且在类定义的主体中可用。

    class MyClass:
        @classmethod
        def make_new(cls) -> __qualname__:
            return cls()
    

    通过这样做,重命名类并不意味着修改类型提示。但我个人并不指望智能代码编辑器能很好地处理这种形式。

    2年前 0条评论
  • user2426679的头像
    user2426679 评论

    编辑:@juanpa.arrivillaga 引起了我的注意,这是一种更好的方法;见https://stackoverflow.com/a/63237226

    建议做上面的答案,而不是下面的这个。

    [下面的旧答案,留给后代]

    我❤️保罗的回答

    但是,关于与 self 相关的类型提示继承有一点需要说明,即如果您通过使用类名的文字复制粘贴作为字符串来键入提示,那么您的类型提示将不会以正确或一致的方式。

    对此的解决方案是通过将类型提示放在函数本身的返回上来提供返回类型提示。

    ✅ 例如,这样做:

    class DynamicParent:
      def func(self):
        # roundabout way of returning self in order to have inherited type hints of the return
        # https://stackoverflow.com/a/64938978
        _self:self.__class__ = self
        return _self
    

    ❌不要这样做:

    class StaticParent:
      def func(self) -> 'StaticParent':
        return self
    

    以下是您想通过上面显示的迂回✅方式进行类型提示的原因

    class StaticChild(StaticParent):
      pass
    
    class DynamicChild(DynamicParent):
      pass
    
    static_child = StaticChild()
    dynamic_child = DynamicChild()
    

    dynamic_child屏幕截图显示在引用自我时类型提示可以正常工作:

    enter image description here

    static_child截图显示类型提示错误地指向父类,即类型提示没有随着继承而正确改变;它是static因为它总是指向父母,即使它应该指向孩子

    enter image description here

    2年前 0条评论