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

2024-11-19 08:38:00
admin
原创
8
摘要:问题描述:我在 Python 3 中有以下代码:class Position: def __init__(self, x: int, y: int): self.x = x self.y = y def __add__(self, other: Positi...

问题描述:

我在 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 的问题。它实际上在其警告和代码完成中使用了这些信息。

IT科技

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


解决方案 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])
相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用