我在 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
但是,如果我错了,请纠正我,并且需要使用其他语法。
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
大概是太麻烦了。
从 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 及更高版本。
__future__
等?
__future__
更多的是关于现在选择加入破坏句法功能,然后在未来的版本中使其成为必需。 (这并不是说第三方库现在无法提供它,但它不会成为现有 Python 版本中标准库的一部分。)
typing_extensions
的一部分提供,但 mypy
还不理解它。此处提供 Python 3.11 跟踪问题:github.com/python/mypy/issues/12840#issue-1244203018
将类型指定为字符串很好,但总是让我有点恼火,因为我们基本上是在绕过解析器。所以你最好不要拼错这些文字字符串中的任何一个:
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)
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)。
如果您只关心修复 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)
from __future__ import annotations
,以至于我可能忘记了。感谢您指出了这一点。我在答案中修复了它。
copy(self: T) -> T
中,这意味着无论您向 copy()
抛出什么对象,copy()
都将始终返回相同类型的对象。在这种情况下,T 是“绑定”到 Postion 的 TypeVar,这意味着“任何类型是 Position
或 Position
的子类”。搜索 TypeVar 以了解更多信息。
Self
可以重复使用?
在解析类主体本身时,名称“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 代替 - 它只会允许使用类名,简单明了,不带任何引号: 鼓舞士气的建议是用懒惰的方式解决。
当基于字符串的类型提示可接受时,也可以使用 __qualname__
项。它包含类的名称,并且在类定义的主体中可用。
class MyClass:
@classmethod
def make_new(cls) -> __qualname__:
return cls()
通过这样做,重命名类并不意味着修改类型提示。但我个人并不指望智能代码编辑器能很好地处理这种形式。
mypy
而言,这不是有效的注释。
编辑:@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
annotated with a type variable bound to the parent class
的问题吗?我不清楚如何将类型变量绑定到引用后续子实例的父类。
不定期副业成功案例分享
typing
导入,因为在评估字符串时,您使用的任何类型都必须在范围内。''
放在类中,而不是类型参数from __future__ import annotations
的任何人的重要提示 - 这必须在所有其他导入之前导入。@classmethod def f(cls) -> CurrentClass:
其中CurrentClass
评估为任何cls
在运行时会是什么?因此,如果A
和B
从实现f
的类继承,那么A.f() -> A
和B.f() -> B
?