繁体   English   中英

如何键入带有封闭 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__方法中)。 我应该如何指定我希望返回类型为Position类型?

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

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

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:延迟评估注释 使用from __future__ import annotations的未来语句的模块将自动将注释存储为字符串:

from __future__ import annotations

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

这已计划成为 Python 3.10 中的默认设置,但现在已推迟此更改。 由于 Python 仍然是一种动态类型语言,因此在运行时不会进行类型检查,因此键入注释应该不会影响性能,对吧? 错误的! 在 Python 3.7 之前,类型化模块曾经是核心中最慢的 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

大概是太麻烦了。

从 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)

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

typing_extensions 模块有两个相关目的:

  • 在较旧的 Python 版本上启用新的类型系统功能。 例如, typing.TypeGuard 是 Python 3.10 中的新功能,但 typing_extensions 允许 Python 3.6 到 3.9 上的用户也可以使用它。
  • 在新类型系统 PEP 被接受并添加到打字模块之前,请先对其进行试验。

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

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

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)

如果您只关心修复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)

在解析类主体本身时,名称“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 :从 Python 3.10 开始,PEP 563 正在重新设计,可能会使用PEP 649代替 - 它只会允许使用类名,简单明了,不带任何引号:pep 建议是以懒惰的方式解决。

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

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

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

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

在此处输入图像描述

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

在此处输入图像描述

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM