如何使用封闭类的类型键入提示方法?

我在Python 3中有以下代码:

class Position:
def __init__(self, x: int, y: int):self.x = xself.y = y
def __add__(self, other: Position) -> Position:return Position(self.x + other.x, self.y + other.y)

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

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

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

220711 次浏览

在解析类主体本身时,名称“位置”是不可用的。我不知道你是如何使用类型声明的,但是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:从Python 3.10开始,PEP 563被重新修改,可能是PEP 649被使用了——它只是允许使用类名,简单,没有任何引号:pep建议是以一种懒惰的方式解决它。

更新3:从Python 3.11(将于2022年底发布)开始,将有为此目的设计的typing.Self可用。检查PEP 673!用于解析前向引用的PEP 563和649,上面提到的仍在竞争,很可能它们都不会像现在这样前进。

太长别读:截至今天(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 563更改的Python。

Python 3.7+:from __future__ import annotations

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

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

这原计划成为Python 3.10中的默认设置,但这一更改现在被推迟了。由于Python仍然是一种动态类型语言,因此在运行时不进行类型检查,键入注释应该不会对性能产生影响,对吧?错!在Python 3.7之前,键入模块曾经是核心中最慢的python模块之一所以对于涉及导入#0模块的代码,当您升级到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 = leftself.right = right

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

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

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

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: Falseother is Position: False

B.猴子补丁以添加注释:

你可能想尝试一些Python元编程魔法并编写一个装饰器对类定义进行猴子补丁以添加注释:

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

装饰师应该负责以下等效工作:

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

至少看起来是对的:

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

可能太麻烦了。

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

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

一个轻微的变化是使用绑定的typevar,至少在声明typevar时只需要编写一次字符串:

from typing import TypeVar
T = TypeVar('T', bound='Position')
class Position:
def __init__(self, x: int, y: int):self.x = xself.y = y
def __add__(self, other: T) -> T:return Position(self.x + other.x, self.y + other.y)

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

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

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

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

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

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

from __future__ import annotations

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

稍微不常见的是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 = xself.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)

编辑:@juanpa.arrivillaga让我注意到一个更好的方法;见https://stackoverflow.com/a/63237226

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

[下面的老答案,留给后人]

❤️保罗的回答

但是,关于type hint继承与self的关系有一点需要注意,那就是如果您通过使用类名的文字复制粘贴作为字符串来键入hint,那么您的type hint将不会以正确或一致的方式继承。

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

✅例如,这样做:

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__ = selfreturn _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屏幕截图显示引用self时类型提示正常工作:

输入图片描述

static_child屏幕截图显示类型提示错误地指向父类,即类型提示不会随着继承而正确更改;它是static,因为它总是指向父类,即使它应该指向子类

输入图片描述

从Python 3.11(将于2022年底发布)开始,您将能够使用#0作为返回类型。

from typing import Self

class Position:
def __init__(self, x: int, y: int):self.x = xself.y = y
def __add__(self, other: Self) -> Self:return Position(self.x + other.x, self.y + other.y)

Self也包含在typing-extensions包中(在PyPi上可用),虽然不是标准库的一部分,但它是typing模块的“预览”版本。从https://pypi.org/project/typing-extensions/开始,

typing_extensions模块有两个相关的目的:

  • 启用使用旧Python版本上的新类型系统功能。例如,TypeGuard是Python 3.10中的新功能,但typing_extensions允许Python 3.6到3.9上的用户也可以使用它。
  • 启用实验在接受并添加到新类型系统PEP之前输入模块。

目前,typing-extensions正式支持Python 3.7及更高版本。