我在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)说引用位置不能解析(在__add__方法中)。我应该如何指定我期望返回类型为Position类型?

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

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


在解析类体本身时,名称'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开始,可以从__future__ import注释中写入注释,以推迟注释的求值。前向引用类应该直接工作。

更新2:从Python 3.10开始,PEP 563正在被重新设计,可能会使用PEP 649来代替——它只允许使用类名,普通的,不带任何引号:PEP的建议是,它以惰性方式解析。

更新3:截至Python 3.11(将于2022年底发布),将有可用的输入。自我设计用于此目的。检查PEP 673!上面提到的解决前向引用的pep 563和649仍然存在争议,很可能它们都不会像现在这样向前推进。


TL;DR:截至今天(2019年),在Python 3.7+中,您可以使用“future”语句从__future__导入注释开启此功能。

(由__future__导入注释启用的行为可能成为Python未来版本的默认值,并将在Python 3.10中成为默认值。然而,3.10的改动在最后一刻被恢复了,现在可能根本不会发生了。)

在Python 3.6或以下版本中,应该使用字符串。


我猜你得到了这个例外:

NameError: name 'Position' is not defined

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

Python 3.7+:从__future__导入注释

Python 3.7引入了PEP 563:注释的延迟求值。使用__future__ import注释中的future语句的模块将自动将注释存储为字符串:

from __future__ import annotations

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

这已被安排成为Python 3.10的默认值,但这一更改现在已被推迟。由于Python仍然是一种动态类型语言,因此在运行时不进行类型检查,因此键入注释应该不会对性能产生影响,对吗?错了!在Python 3.7之前,类型模块曾经是核心中最慢的Python模块之一,因此对于涉及导入类型模块的代码,当您升级到3.7时,性能将提高7倍。

Python <3.7:使用字符串

根据PEP 484,您应该使用字符串而不是类本身:

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

如果你使用Django框架,这可能很熟悉,因为Django模型也使用字符串进行前向引用(外部模型是self或尚未声明的外键定义)。这应该与Pycharm和其他工具一起工作。

来源

PEP 484和PEP 563的相关部分,以避免您的旅程:

Forward references When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later. A situation where this occurs commonly is the definition of a container class, where the class being defined occurs in the signature of some of the methods. For example, the following code (the start of a simple binary tree implementation) does not work: class Tree: def __init__(self, left: Tree, right: Tree): self.left = left self.right = right To address this, we write: class Tree: def __init__(self, left: 'Tree', right: 'Tree'): self.left = left self.right = right The string literal should contain a valid Python expression (i.e., compile(lit, '', 'eval') should be a valid code object) and it should evaluate without errors once the module has been fully loaded. The local and global namespace in which it is evaluated should be the same namespaces in which default arguments to the same function would be evaluated.

PEP 563:

Implementation In Python 3.10, function and variable annotations will no longer be evaluated at definition time. Instead, a string form will be preserved in the respective __annotations__ dictionary. Static type checkers will see no difference in behavior, whereas tools using annotations at runtime will have to perform postponed evaluation. ... Enabling the future behavior in Python 3.7 The functionality described above can be enabled starting from Python 3.7 using the following special import: 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)

decorator应该负责如下内容:

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

可能太麻烦了。


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

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)

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

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

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


如果你只关心修复NameError: name 'Position'没有定义,你可以指定类名为字符串:

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)

编辑:@juanpa。Arrivillaga让我注意到一种更好的方法;参见https://stackoverflow.com/a/63237226

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

[旧答案如下,为后人保留]

我❤️保罗的回答

然而,关于类型提示继承与self的关系,有一点需要说明,即如果您通过使用类名作为字符串的文字复制粘贴来键入hint,那么您的类型提示将不会以正确或一致的方式继承。

解决方案是通过在函数本身的返回值上放置类型提示来提供返回类型提示。

✅例如,这样做:

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截图显示类型提示在引用self时正确工作:

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


从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也包含在类型扩展包中(在PyPi上可用),尽管它不是标准库的一部分,但它是类型模块的某种“预览”版本。从https://pypi.org/project/typing-extensions/,

typing_extensions模块有两个相关的用途: 允许使用 旧版本Python上的新类型系统特性。例如, 打字。TypeGuard是Python 3.10中的新功能,但typing_extensions允许 Python 3.6到3.9版本的用户也可以使用它。 使实验 在新类型系统pep被接受并添加到 输入模块。

目前,类型扩展正式支持Python 3.7及更高版本。