【问题标题】:How do I type hint a method with the type of the enclosing class?如何键入提示具有封闭类类型的方法?
【发布时间】:2022-01-04 10:56:03
【问题描述】:

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

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

【问题讨论】:

    标签: python python-3.x pycharm type-hinting python-typing


    【解决方案1】:

    在解析类主体本身时,名称“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】:

      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+:from __future__ import annotations

      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

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

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

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

      来源

      PEP 484 和 PEP 563 的相关部分,省去你的旅行:

      Forward references

      当类型提示包含尚未定义的名称时,该定义可以表示为字符串文字,稍后再解析。

      这种情况经常发生的情况是定义容器类,其中被定义的类出现在某些方法的签名中。例如,下面的代码(一个简单的二叉树实现的开始)不起作用:

      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:

      Implementation

      在 Python 3.10 中,函数和变量注释将不再在定义时进行评估。相反,字符串形式将保留在相应的 __annotations__ 字典中。静态类型检查器不会发现行为差异,而在运行时使用注释的工具将不得不执行延迟评估。

      ...

      Enabling the future behavior in 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 元编程魔法并编写一个装饰器 对类定义进行猴子补丁以添加注释:

      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
      

      可能太麻烦了。

      【讨论】:

      • 对,这与其说是 PyCharm 问题,不如说是 Python 3.5 PEP 484 问题。我怀疑如果你通过 mypy 类型工具运行它,你会得到同样的警告。
      • @JoelBerkeley 我刚刚对其进行了测试,并且类型参数在 3.6 上为我工作,只是不要忘记从 typing 导入,因为在评估字符串时,您使用的任何类型都必须在范围内。
      • 啊,我的错,我只是把 '' 放在类周围,而不是类型参数
      • 对使用 from __future__ import annotations 的任何人的重要说明 - 这必须在所有其他导入之前导入。
      • 有没有办法指定函数的返回类型是当前类,不管它是什么?例如,@classmethod def f(cls) -> CurrentClass: 其中CurrentClass 的计算结果是cls 在运行时会是什么?这样如果AB继承自实现f的类,那么A.f() -> AB.f() -> B
      【解决方案3】:

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

      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)
      

      【讨论】:

      • 我希望 Python 有一个 typing.Self 来明确指定这一点。
      • 我来这里是想看看是否存在像您的typing.Self 这样的东西。在利用多态性时,返回硬编码字符串无法返回正确的类型。在我的例子中,我想实现一个 deserialize 类方法。我决定返回一个字典(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)。
      【解决方案4】:

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

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

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

      【讨论】:

      • 这特别有用,因为它不会对类名进行硬编码,因此它可以继续在子类中工作。
      • 我不确定这是否适用于注释的延迟评估 (PEP 563),所以我有 asked a question for that
      • 请注意,就mypy 而言,这不是一个有效的注释。
      • this solution 以不同的方式修复硬编码
      • @user2426679 这个答案和您引用的答案都不是有效的类型注释。在此处使用绑定的 typevar 方法:stackoverflow.com/a/63237226/5014455
      【解决方案5】:

      如果您只关心修复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)
      

      【讨论】:

      • @Arjan。你说的对。我已经习惯了from __future__ import annotations,以至于我可能忘记了。感谢您指出了这一点。我在答案中修复了它。
      • 字母“T”是什么?
      • @Eldosa:“T”被定义为 TypeVar。将其视为“任何类型”。在copy(self: T) -> T 的定义中,这意味着无论您向copy()copy() 抛出什么对象,都将始终返回相同类型的对象。在这种情况下,T 是“绑定”到 Postion 的 TypeVar,这意味着“任何类型,要么是 Position,要么是 Position 的子类”。搜索 TypeVar 以了解更多信息。
      • 有什么巧妙的技巧可以让通用的Self 可以重复使用吗?
      • @classmethod 看起来如何?
      【解决方案6】:

      编辑:@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 截图显示类型提示在引用自我时可以正常工作:

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

      【讨论】:

      • 这不是一个有效的类型注解,也不是正确的类型注解方式来注解您要表达的内容,应该使用绑定到父类的类型变量进行注解
      • @juanpa.arrivillaga 你能回答annotated with a type variable bound to the parent class这个问题吗?我不清楚如何将类型变量绑定到引用后续子实例的父类。
      【解决方案7】:

      从 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 Position(self.x + other.x, self.y + other.y)
      

      【讨论】:

        猜你喜欢
        相关资源
        最近更新 更多