如何使用封闭类的类型来提示方法的类型?
- 2024-11-19 08:38:00
- admin 原创
- 33
问题描述:
我在 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)
Position
但是我的编辑器(PyCharm)说无法解析引用(在__add__
方法中)。我应该如何指定我期望返回类型为 类型Position
?
我认为这实际上是 PyCharm 的问题。它实际上在其警告和代码完成中使用了这些信息。
但如果我错了,请纠正我,并且需要使用其他语法。
解决方案 1:
TL;DR:截至今天(2019 年),在 Python 3.7+ 中,您可以使用“future”语句来启用此功能from __future__ import annotations
。
(通过启用的行为from __future__ import annotations
计划在 Python 3.14 中成为默认行为,即“延迟评估注释”。它将在Python 3.10 中成为默认行为,即“推迟评估注释”。然而,3.10 中的更改在最后一刻被恢复了。)
在 Python 3.6 或更低版本中,您应该使用字符串。
我猜你遇到了这个异常:
NameError: name 'Position' is not defined
这是因为Position
必须先定义它才能在注释中使用它,除非您使用启用了PEP 563更改的 Python。
Python 3.11+:from typing import Self
from typing import Self
class Position:
def __add__(self, other: Self) -> Self:
...
对于 Python 版本 <3.11,你可以使用:
from typing_extensions import Self
参考:
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 之前,类型模块曾经是核心中最慢的 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 元编程魔法并编写一个装饰器来对类定义进行 monkey-patch 以添加注释:
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
可能太麻烦了。
解决方案 2:
PEP 673在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 type(self)(self.x + other.x, self.y + other.y)
返回Self
通常是一个好主意,但是您必须返回与 相同类型的对象self
,这意味着调用type(self)
而不是Position
。
对于较旧版本的 Python(目前为 3.7 及更高版本),请使用typing-extensions
包。其目的之一是
在旧版 Python 上启用新类型系统功能。例如,
typing.TypeGuard
是 Python 3.10 中的新功能,但也typing_extensions
允许旧版 Python 的用户使用它。
然后您只需从 而typing_extensions
不是导入typing
,例如from typing_extensions import Self
。
解决方案 3:
从 Python 3.11(2022 年底发布)开始,有专typing.Self
为此目的而设计的版本。查看PEP 673!
对于以前的 Python 版本,必须考虑到在解析类主体本身时名称“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 提议是以一种懒惰的方式解决它。
更新 3:从 Python 3.11 开始,上面提到的用于解析前向引用的 PEP 563 和 649 仍然存在竞争,而且很可能都无法像现在这样继续前进。
解决方案 4:
将类型指定为字符串是可以的,但总是让我有点恼火,因为我们基本上是在绕过解析器。所以你最好不要拼错这些文字字符串中的任何一个:
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)
解决方案 5:
如果您只关心修复NameError: name 'Position' is not defined
,您可以将类名指定为字符串:
def __add__(self, other: 'Position') -> 'Position':
或者,如果您使用 Python 3.7 或更高版本,请将以下行添加到代码顶部(就在其他导入之前)
from __future__ import annotations
从技术上讲,这会将所有注释转换为字符串。从 Python 3.14 开始,这不再是必要的,因为 Python 3.14 引入了注释的延迟求值1。
但是,如果您还希望它适用于子类并返回特定的子类,则需要将该方法注释为通用方法Self
(从 Python 3.11 开始)或使用TypeVar
(自 Python 3.5 起可用)。
from __future__ import annotations
class Position:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __add__(self, other: Position) -> Self:
return type(self)(self.x + other.x, self.y + other.y)
def copy(self) -> Self:
return type(self)(self.x, self.y)
如果要支持 3.11 之前的版本,请使用。基本上,这种类型提示会告诉类型检查器和TypeVar
的返回类型与的类型相同。__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)
解决方案 6:
当基于字符串的类型提示可接受时,__qualname__
也可以使用该项目。它包含类的名称,并且在类定义主体中可用。
class MyClass:
@classmethod
def make_new(cls) -> __qualname__:
return cls()
通过这种方式,重命名类并不意味着修改类型提示。但我个人并不指望智能代码编辑器能很好地处理这种形式。
解决方案 7:
编辑:@juanpa.arrivillaga 让我注意到了一种更好的方法;请参阅https://stackoverflow.com/a/63237226
建议按照上面的答案来做,而不是按照下面的答案。
[以下是旧答案,供后人保留]
我❤️Paulo的回答
但是,关于与自身相关的类型提示继承,有一点需要说明,那就是如果你通过将类名的文字复制粘贴作为字符串来进行类型提示,那么你的类型提示将无法以正确或一致的方式继承。
解决这个问题的方法是,通过在函数本身的返回上放置类型提示来提供返回类型提示。
✅例如这样做:
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
因为它总是指向父类,即使它应该指向子类
解决方案 8:
对于 Python 3.11+,'typings' 模块中有一个 'Self' 类型提示。
对于可能像我一样偶然发现这个问题的 Cython/mypyc 用户来说,这没关系。编译器足够聪明,可以推断出函数参数中指定的类型对应于封闭类的类型
解决方案 9:
使用:
from __future__ import annotations
import sys
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
class Animal:
def __init__(self, name: str, says: str) -> None:
self.name = name
self.says = says
@classmethod
def from_description(cls, description: str = "|") -> Self:
descr = description.split("|")
return cls(descr[0], descr[1])
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 项目管理必备:盘点2024年13款好用的项目管理软件