ChatGPT解决这个技术问题 Extra ChatGPT

如何键入提示具有封闭类类型的方法?

我在 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://i.imgur.com/yjjCWw3.png

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


M
Marco Bonelli

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

from __future__ import annotations 可能 启用的行为在 Python 的未来版本中成为默认值,并且 was going 在 Python 3.10 中成为默认值。但是,3.10 was reverted 中的更改最后分钟,现在可能根本不会发生。)

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

我猜你有这个例外:

NameError: name 'Position' is not defined

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

Python 3.7+:从 __future__ 导入注释

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 之前,打字模块曾经是 one of the slowest python modules in core,所以对于涉及导入 typing 模块的代码,升级到 3.7 时会看到一个 up to 7 times increase in performance

Python <3.7:使用字符串

According to 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. 定义一个虚拟位置

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

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

大概是太麻烦了。


对,这与其说是 PyCharm 问题,不如说是 Python 3.5 PEP 484 问题。如果您通过 mypy 类型工具运行它,我怀疑您会收到相同的警告。
@JoelBerkeley 我刚刚对其进行了测试,并且类型参数在 3.6 上为我工作,只是不要忘记从 typing 导入,因为在评估字符串时,您使用的任何类型都必须在范围内。
啊,我的错误,我只是把 '' 放在类中,而不是类型参数
对使用 from __future__ import annotations 的任何人的重要提示 - 这必须在所有其他导入之前导入。
有没有办法指定函数的返回类型是当前类,不管它是什么?例如,@classmethod def f(cls) -> CurrentClass: 其中 CurrentClass 评估为任何 cls 在运行时会是什么?因此,如果 AB 从实现 f 的类继承,那么 A.f() -> AB.f() -> B
3
3 revs

从 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 及更高版本。


在 Python 3.11 中,这个解决方案变得最不笨拙和最简洁。
他们有没有机会将此移植到 __future__ 等?
没有。__future__ 更多的是关于现在选择加入破坏句法功能,然后在未来的版本中使其成为必需。 (这并不是说第三方库现在无法提供它,但它不会成为现有 Python 版本中标准库的一部分。)
我相信它已经作为 typing_extensions 的一部分提供,但 mypy 还不理解它。此处提供 Python 3.11 跟踪问题:github.com/python/mypy/issues/12840#issue-1244203018
@cj81499 好点,我忘了检查那个模块。
v
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)

我希望 Python 有一个 typing.Self 来明确指定这一点。
我来这里是想看看是否存在像您的 typing.Self 这样的东西。在利用多态性时,返回硬编码字符串无法返回正确的类型。就我而言,我想实现一个 deserialize 类方法。我决定返回一个 dict (kwargs) 并调用 some_class(**some_class.deserialize(raw_data))
当正确实现它以使用子类时,此处使用的类型注释是适当的。但是,实现返回 Position,而不是类,因此上面的示例在技术上是不正确的。实现应将 Position( 替换为 self.__class__( 之类的内容。
此外,注释说返回类型取决于 other,但很可能它实际上取决于 self。因此,您需要将注释放在 self 上以描述正确的行为(也许 other 应该只是 Position 以表明它与返回类型无关)。这也可用于仅使用 self 的情况。例如def __aenter__(self: T) -> T:
typing.Self 将在 Python 3.11 中可用(根据 PEP-673)。
A
Alex Waygood

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

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

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

from __future__ import annotations

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

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

@Arjan。你说的对。我已经习惯了from __future__ import annotations,以至于我可能忘记了。感谢您指出了这一点。我在答案中修复了它。
字母“T”是什么?
@Eldosa:“T”被定义为 TypeVar。将其视为“任何类型”。在定义 copy(self: T) -> T 中,这意味着无论您向 copy() 抛出什么对象,copy() 都将始终返回相同类型的对象。在这种情况下,T 是“绑定”到 Postion 的 TypeVar,这意味着“任何类型是 PositionPosition 的子类”。搜索 TypeVar 以了解更多信息。
是否有任何巧妙的技巧可以使通用的 Self 可以重复使用?
这如何寻找@classmethod?
j
jsbueno

在解析类主体本身时,名称“Position”不可用。我不知道你是如何使用类型声明的,但是 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 - 符合该标准的工具将知道从那里解开类名并使用它。 (记住 Python 语言本身对这些注释没有任何作用总是很重要的。它们通常用于静态代码分析,或者可以有一个库/框架用于在运行时进行类型检查 - 但您必须明确设置。)

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

更新 2:从 Python 3.10 开始,PEP 563 正在重新编写,可能会使用 PEP 649 代替 - 它只会允许使用类名,简单明了,不带任何引号: 鼓舞士气的建议是用懒惰的方式解决。


Y
Yvon DUTAPIS

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

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

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


这特别有用,因为它不会对类名进行硬编码,因此它可以继续在子类中工作。
我不确定这是否适用于注释的延迟评估 (PEP 563),所以我有 asked a question for that
请注意,就 mypy 而言,这不是有效的注释。
this solution 以不同的方式修复硬编码
@user2426679 这个答案和您引用的答案都不是有效的类型注释。在此处使用绑定 typevar 方法:stackoverflow.com/a/63237226/5014455
u
user2426679

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

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

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

我❤️ Paulo's answer

但是,关于与 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 屏幕截图显示类型提示在引用自我时可以正常工作:

https://i.stack.imgur.com/BkfUW.png

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

https://i.stack.imgur.com/IFOLK.png


这不是一个有效的类型注解,也不是正确的类型注解你试图表达的方式,应该用绑定到父类的类型变量来注解
@juanpa.arrivillaga 你能回答这个annotated with a type variable bound to the parent class的问题吗?我不清楚如何将类型变量绑定到引用后续子实例的父类。