简体   繁体   English

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

[英]How do I type hint a method with the type of the enclosing class?

I have the following code in Python 3:我在 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)

But my editor (PyCharm) says that the reference Position can not be resolved (in the __add__ method).但是我的编辑器 (PyCharm) 说引用Position无法解析(在__add__方法中)。 How should I specify that I expect the return type to be of type Position ?我应该如何指定我希望返回类型为Position类型?

Edit: I think this is actually a PyCharm issue.编辑:我认为这实际上是一个 PyCharm 问题。 It actually uses the information in its warnings, and code completion.它实际上在其警告和代码完成中使用了这些信息。

But correct me if I'm wrong, and need to use some other syntax.但如果我错了,请纠正我,并且需要使用其他语法。

TL;DR : As of today (2019), in Python 3.7+ you can turn this feature on using a "future" statement, from __future__ import annotations . TL;DR :截至今天(2019 年),在 Python 3.7+ 中,您可以使用“未来”语句from __future__ import annotations打开此功能。

(The behaviour enabled by from __future__ import annotations might become the default in future versions of Python, and was going to be made the default in Python 3.10. However, the change in 3.10 was reverted at the last minute, and now may not happen at all.) (由from __future__ import annotations启用的行为可能会成为 Python 未来版本的默认设置,并且在 Python 3.10 中成为默认设置。但是,3.10 中的更改在最后一刻被恢复,现在可能不会发生在全部。)

In Python 3.6 or below, you should use a string.在 Python 3.6 或更低版本中,您应该使用字符串。


I guess you got this exception:我猜你有这个例外:

NameError: name 'Position' is not defined

This is because Position must be defined before you can use it in an annotation, unless you are using Python with PEP 563 changes enabled.这是因为必须先定义Position ,然后才能在注释中使用它,除非您使用启用了PEP 563更改的 Python。

Python 3.7+: from __future__ import annotations Python 3.7+: from __future__ import annotations

Python 3.7 introduces PEP 563: postponed evaluation of annotations . Python 3.7 引入了PEP 563:延迟评估注释 A module that uses the future statement from __future__ import annotations will store annotations as strings automatically:使用from __future__ import annotations的未来语句的模块将自动将注释存储为字符串:

from __future__ import annotations

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

This had been scheduled to become the default in Python 3.10, but this change has now been postponed.这已计划成为 Python 3.10 中的默认设置,但现在已推迟此更改。 Since Python still is a dynamically typed language so no type-checking is done at runtime, typing annotations should have no performance impact, right?由于 Python 仍然是一种动态类型语言,因此在运行时不会进行类型检查,因此键入注释应该不会影响性能,对吧? Wrong!错误的! Before Python 3.7, the typing module used to be one of the slowest python modules in core so for code that involves importing the typing module, you will see an up to 7 times increase in performance when you upgrade to 3.7.在 Python 3.7 之前,类型化模块曾经是核心中最慢的 python 模块之一,因此对于涉及导入typing化模块的代码,当升级到 3.7 时,您将看到性能提升多达 7 倍

Python <3.7: use a string Python <3.7:使用字符串

According to PEP 484 , you should use a string instead of the class itself:根据 PEP 484 ,您应该使用字符串而不是类本身:

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

If you use the Django framework, this may be familiar, as Django models also use strings for forward references (foreign key definitions where the foreign model is self or is not declared yet).如果您使用 Django 框架,这可能很熟悉,因为 Django 模型也使用字符串进行前向引用(外键定义,其中外部模型是self或尚未声明)。 This should work with Pycharm and other tools.这应该适用于 Pycharm 和其他工具。

Sources来源

The relevant parts of PEP 484 and PEP 563, to spare you the trip: 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 (ie, compile(lit, '', 'eval') should be a valid code object) and it should evaluate without errors once the module has been fully loaded.字符串文字应该包含一个有效的 Python 表达式(即 compile(lit, '', 'eval') 应该是一个有效的代码对象),并且一旦模块完全加载,它应该没有错误地进行评估。 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.评估它的本地和全局命名空间应该是相同的命名空间,其中将评估同一函数的默认参数。

and PEP 563:和 PEP 563:

Implementation执行

In Python 3.10, function and variable annotations will no longer be evaluated at definition time.在 Python 3.10 中,函数和变量注释将不再在定义时进行评估。 Instead, a string form will be preserved in the respective __annotations__ dictionary.相反,字符串形式将保存在各自的__annotations__字典中。 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 在 Python 3.7 中启用未来行为

The functionality described above can be enabled starting from Python 3.7 using the following special import:从 Python 3.7 开始,可以使用以下特殊导入启用上述功能:

 from __future__ import annotations

Things that you may be tempted to do instead你可能想做的事情

A. Define a dummy Position A. 定义一个虚拟Position

Before the class definition, place a dummy definition:在类定义之前,放置一个虚拟定义:

class Position(object):
    pass


class Position(object):
    ...

This will get rid of the NameError and may even look OK:这将摆脱NameError甚至看起来不错:

>>> Position.__add__.__annotations__
{'other': __main__.Position, 'return': __main__.Position}

But is it?但是是吗?

>>> 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 in order to add the annotations: B. Monkey-patch 为了添加注释:

You may want to try some Python metaprogramming magic and write a decorator to monkey-patch the class definition in order to add annotations:您可能想尝试一些 Python 元编程魔法并编写一个装饰器来猴子修补类定义以添加注释:

class Position:
    ...
    def __add__(self, other):
        return self.__class__(self.x + other.x, self.y + other.y)

The decorator should be responsible for the equivalent of this:装饰者应该负责相当于:

Position.__add__.__annotations__['return'] = Position
Position.__add__.__annotations__['other'] = Position

At least it seems right:至少看起来是对的:

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: True
other is Position: True

Probably too much trouble.大概是太麻烦了。

Starting in Python 3.11 (to be released in late 2022), you'll be able to use Self as the return type.从 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 is also included in the typing-extensions package (available on PyPi), which although not part of the standard library, is sort of a "preview" version of the typing module. Self也包含在typing-extensions包中(在 PyPi 上可用),虽然不是标准库的一部分,但它是typing模块的“预览”版本。 From https://pypi.org/project/typing-extensions/ ,https://pypi.org/project/typing-extensions/

The typing_extensions module serves two related purposes: typing_extensions 模块有两个相关目的:

  • Enable use of new type system features on older Python versions.在较旧的 Python 版本上启用新的类型系统功能。 For example, typing.TypeGuard is new in Python 3.10, but typing_extensions allows users on Python 3.6 through 3.9 to use it too.例如, typing.TypeGuard 是 Python 3.10 中的新功能,但 typing_extensions 允许 Python 3.6 到 3.9 上的用户也可以使用它。
  • Enable experimentation with new type system PEPs before they are accepted and added to the typing module.在新类型系统 PEP 被接受并添加到打字模块之前,请先对其进行试验。

Currently, typing-extensions officially supports Python 3.7 and later.目前, typing-extensions正式支持 Python 3.7 及更高版本。

Specifying the type as string is fine, but always grates me a bit that we are basically circumventing the parser.将类型指定为字符串很好,但总是让我有点恼火,因为我们基本上是在绕过解析器。 So you better not misspell any one of these literal strings:所以你最好不要拼错这些文字字符串中的任何一个:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

A slight variation is to use a bound typevar, at least then you have to write the string only once when declaring the typevar:一个细微的变化是使用绑定的类型变量,至少在声明类型变量时您只需编写一次字符串:

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)

If you only care about fixing the NameError: name 'Position' is not defined , you can either specify the class name as a string:如果您只关心修复NameError: name 'Position' is not defined ,您可以将类名指定为字符串:

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

Or if you use Python 3.7 or higher, add the following line to the top of your code (just before the other imports)或者,如果您使用 Python 3.7 或更高版本,请将以下行添加到代码顶部(就在其他导入之前)

from __future__ import annotations

However, if you also want this to work for subclasses, and return the specific subclass, you need to annotate the method as being a generic method , by using a TypeVar .但是,如果您还希望它适用于子类并返回特定的子类,则需要使用TypeVar将该方法注释为泛型方法

What is slightly uncommon is that the TypeVar is bound to the type of self .稍微不常见的是TypeVar绑定到self的类型。 Basically, this typing hinting tells the type checker that the return type of __add__() and copy() are the same type as 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)

The name 'Position' is not avalilable at the time the class body itself is parsed.在解析类主体本身时,名称“Position”不可用。 I don't know how you are using the type declarations, but Python's PEP 484 - which is what most mode should use if using these typing hints say that you can simply put the name as a string at this point:我不知道你是如何使用类型声明的,但是 Python 的 PEP 484 - 如果使用这些输入提示说你可以简单地将名称作为字符串放在这一点上,这是大多数模式应该使用的:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Check the PEP 484 section on forward references - tools conforming to that will know to unwrap the class name from there and make use of it.检查 PEP 484关于前向引用的部分- 符合该标准的工具将知道从那里解开类名并使用它。 (It is always important to have in mind that the Python language itself does nothing with these annotations. They are usually meant for static-code analysis, or one could have a library/framework for type-checking at runtime - but you have to explicitly set that.) (始终重要的是要记住 Python 语言本身对这些注释没有任何作用。它们通常用于静态代码分析,或者可以有一个库/框架用于在运行时进行类型检查 - 但您必须明确设置。)

Update : Also, as of Python 3.7, check out PEP 563 .更新:另外,从 Python 3.7 开始,请查看PEP 563 As of Python 3.8, it is possible to write from __future__ import annotations to defer the evaluation of annotations.从 Python 3.8 开始,可以编写from __future__ import annotations来推迟对注解的评估。 Forward-referencing classes should work straightforward.前向引用类应该直接工作。

Update 2 : As of Python 3.10, PEP 563 is being retought, and it may be that PEP 649 is used in instead - it would simply allow the class name to be used, plain, without any quotes: the pep proposal is that it is resolved in a lazy way.更新 2 :从 Python 3.10 开始,PEP 563 正在重新设计,可能会使用PEP 649代替 - 它只会允许使用类名,简单明了,不带任何引号:pep 建议是以懒惰的方式解决。

When a string-based type hint is acceptable, the __qualname__ item can also be used.当基于字符串的类型提示可以接受时,也可以使用__qualname__项。 It holds the name of the class, and it is available in the body of the class definition.它包含类的名称,并且在类定义的主体中可用。

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

By doing this, renaming the class does not imply modifying the type hints.通过这样做,重命名类并不意味着修改类型提示。 But I personally would not expect smart code editors to handle this form well.但我个人并不指望智能代码编辑器能很好地处理这种形式。

edit: @juanpa.arrivillaga brought to my attention a better way to do this;编辑:@juanpa.arrivillaga 引起了我的注意,这是一种更好的方法; see https://stackoverflow.com/a/63237226https://stackoverflow.com/a/63237226

It's recommended to do the above answer instead of this one below.建议做上面的答案,而不是下面的这个。

[ old answer below, kept for posterity ] [下面的旧答案,留给后代]

I ❤️ Paulo's answer我❤️保罗的回答

However, there's a point to be made about type hint inheritance in relation to self, which is that if you type hint by using a literal copy paste of the class name as a string, then your type hint won't inherit in a correct or consistent way.但是,关于与 self 相关的类型提示继承有一点需要说明,即如果您通过使用类名的文字复制粘贴作为字符串来键入提示,那么您的类型提示将不会以正确或一致的方式。

The solution to this is to provide return type hint by putting the type hint on the return in the function itself.对此的解决方案是通过将类型提示放在函数本身的返回上来提供返回类型提示。

✅ For example, do this: ✅ 例如,这样做:

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

Instead of doing this: 而不是这样做:

class StaticParent:
  def func(self) -> 'StaticParent':
    return self

Below is the reason why you want to do the type hint via the roundabout ✅ way shown above以下是您想通过上面显示的迂回✅方式进行类型提示的原因

class StaticChild(StaticParent):
  pass

class DynamicChild(DynamicParent):
  pass

static_child = StaticChild()
dynamic_child = DynamicChild()

dynamic_child screenshot shows that type hinting works correctly when referencing the self: dynamic_child屏幕截图显示类型提示在引用自我时可以正常工作:

在此处输入图像描述

static_child screenshot shows that type hinting is mistakenly pointing at the parent class, ie the type hint does not change correctly with inheritance; static_child截图显示类型提示错误地指向父类,即类型提示没有随着继承而正确改变; it is static because it will always point at the parent even when it should point at the child它是static的,因为它总是指向父级,即使它应该指向子级

在此处输入图像描述

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM