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

How do I type hint a method with the type of the enclosing class?

我在 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__ 方法中)。我应该如何指定我希望 return 类型为 Position 类型?

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

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

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

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

检查 PEP 484 section on forward references - 符合该要求的工具将知道从那里解包 class 名称并使用它。 (一定要记住,Python 语言本身对这些注释没有任何作用。它们通常用于静态代码分析,或者可以有一个 library/framework 用于运行时的类型检查- 但你必须明确地设置它。)

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

更新 2:从 Python 3.10 开始,PEP 563 正在重新修订,可能会改用 PEP 649 - 它会简单地允许使用 class 名称,简单明了,不带任何引号:鼓舞人心的建议是以惰性方式解析它。

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

(由 from __future__ import annotations 启用的行为可能 成为 Python 和 was going to be made the default in Python 3.10. However, the change in 3.10 was reverted 未来版本的默认设置,现在可能根本不会发生。)

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


我猜你遇到了这个例外:

NameError: name 'Position' is not defined

这是因为必须先定义 Position 才能在注释中使用它,除非您使用 Python 并启用了 PEP 563 更改。

Python 3.7+: from __future__ import annotations

Python 3.7 引入了 PEP 563: postponed evaluation of annotations。使用 future 语句 from __future__ import annotations 的模块将自动将注释存储为字符串:

from __future__ import annotations

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

这已计划成为 Python 3.10 中的默认设置,但现在已推迟此更改。由于 Python 仍然是一种动态类型语言,因此在运行时不进行类型检查,所以类型注释应该不会对性能产生影响,对吧?错误的!在Python 3.7之前,升级到3.7时打字模块曾经是one of the slowest python modules in core so for code that involves importing the typing module, you will see an up to 7 times increase in performance

Python <3.7: 使用字符串

According to PEP 484,你应该使用字符串而不是 class 本身:

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

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

来源

PEP 484 和 PEP 563 的相关部分,免你一趟:

Forward references

When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.

A situation where this occurs commonly is the definition of a container class, where the class being defined occurs in the signature of some of the methods. For example, the following code (the start of a simple binary tree implementation) does not work:

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

To address this, we write:

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

The string literal should contain a valid Python expression (i.e., compile(lit, '', 'eval') should be a valid code object) and it should evaluate without errors once the module has been fully loaded. The local and global namespace in which it is evaluated should be the same namespaces in which default arguments to the same function would be evaluated.

和 PEP 563:

Implementation

In Python 3.10, function and variable annotations will no longer be evaluated at definition time. Instead, a string form will be preserved in the respective __annotations__ dictionary. Static type checkers will see no difference in behavior, whereas tools using annotations at runtime will have to perform postponed evaluation.

...

Enabling the future behavior in Python 3.7

The functionality described above can be enabled starting from Python 3.7 using the following special import:

from __future__ import annotations

您可能想做的事情

一个。定义一个虚拟 Position

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

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

乙。猴子补丁为了添加注释:

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

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

可能太麻烦了。

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

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)

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

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

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

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

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

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

from __future__ import annotations

但是,如果您还希望它适用于 subclasses,并且 return 特定的 subclass,您需要将该方法注释为 generic method, 通过使用 TypeVar.

稍微不常见的是 TypeVar 绑定到 self 的类型。基本上,此类型提示告诉类型检查器 __add__()copy() 的 return 类型与 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)

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

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

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

我❤️

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

解决这个问题的方法是通过将类型提示放在函数本身的 return 上来提供 return 类型提示。

✅ 例如,这样做:

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

而不是这样做:

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

下面是你想通过上面显示的roundabout✅方式进行类型提示的原因

class StaticChild(StaticParent):
  pass

class DynamicChild(DynamicParent):
  pass

static_child = StaticChild()
dynamic_child = DynamicChild()

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

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

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

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)

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

The typing_extensions module serves two related purposes:

  • Enable use of new type system features on older Python versions. For example, typing.TypeGuard is new in Python 3.10, but typing_extensions allows users on Python 3.6 through 3.9 to use it too.
  • Enable experimentation with new type system PEPs before they are accepted and added to the typing module.

目前typing-extensions官方支持Python3.7及更高版本。