893

我在 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__方法中)。我应该如何指定我期望返回类型是 type Position

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

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

4

7 回答 7

1249

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 563更改的 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 模块之一,因此对于涉及导入模块的代码,升级到 3.7后typing,您将看到性能提升多达 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

大概是太麻烦了。

于 2015-11-04T22:44:33.850 回答
36

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

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)
于 2018-02-11T19:45:08.450 回答
20

在解析类主体本身时,名称“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来推迟对注释的评估。前向引用类应该直接工作。

于 2015-11-04T22:44:01.020 回答
20

如果您只关心修复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 = 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)
于 2020-08-03T21:32:29.473 回答
14

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

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

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

于 2019-11-10T13:59:32.363 回答
10

从 Python 3.11 开始,您将能够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)
于 2022-01-31T19:58:52.687 回答
8

编辑:@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屏幕截图显示在引用 self 时类型提示可以正常工作:

在此处输入图像描述

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

在此处输入图像描述

于 2020-11-21T01:33:03.943 回答