【问题标题】:How to indicate to mypy an object has certain attributes?如何向 mypy 指示对象具有某些属性?
【发布时间】:2021-11-18 17:19:00
【问题描述】:

我正在使用一些从父类 (Widget) 派生的类;在孩子们中,有些人具有某些属性(posxposy),有些则没有。

import enum
from dataclasses import dataclass
from typing import List


class Color(enum.IntEnum):
    GLOWING_IN_THE_DARK = enum.auto()
    BROWN_WITH_RAINBOW_DOTS = enum.auto()


@dataclass
class Widget:
    """Generic class for widget"""


@dataclass
class Rectangle(Widget):
    """A Color Rectangle"""

    posx: int
    posy: int
    width: int = 500
    height: int = 200
    color: Color = Color.BROWN_WITH_RAINBOW_DOTS


@dataclass
class Group(Widget):
    children: List[Widget]


@dataclass
class Button(Widget):
    """A clickable button"""

    posx: int
    posy: int
    width: int = 200
    height: int = 100
    label: str = "some label"

即使只对具有这些属性的小部件进行了一些过滤,mypy 也无法识别它们应该具有。

有没有办法向mypy 表明我们有一个具有给定属性的对象?

例如下面的函数和调用:

def some_function_that_does_something(widgets: List[Widget]):
    """A useful docstring that says what the function does"""
    widgets_with_pos = [w for w in widgets if hasattr(w, "posx") and hasattr(w, "posy")]

    if not widgets_with_pos:
        raise AttributeError(f"No widget with position found among list {widgets}")

    first_widget = widgets_with_pos[0]
    pos_x = first_widget.posx
    pos_y = first_widget.posy
    print(f"Widget {first_widget} with position: {(pos_x, pos_y)}")


some_widgets = [Group([Rectangle(0, 0)]), Button(10, 10, label="A button")]
some_function_that_does_something(some_widgets)

会按预期返回结果:Widget Button(posx=10, posy=10, width=200, height=100, label='A button') with position: (10, 10)

但是mypy 会抱怨:

__check_pos_and_mypy.py:53: error: "Widget" has no attribute "posx"
        pos_x = first_widget.posx
                ^
__check_pos_and_mypy.py:54: error: "Widget" has no attribute "posy"
        pos_y = first_widget.posy
                ^
Found 2 errors in 1 file (checked 1 source file)

怎么办?

也许,一种方法是改变类的设计:

  • Widget 的子类和位置(例如WidgetWithPos
  • RectangleButton 将派生自此类
  • 我们在函数中注明:widget_with_pos: List[WidgetWithPos] = ...

...但是,我无法更改课程的原始设计,mypy 可能仍会抱怨以下内容:

List comprehension has incompatible type List[Widget]; expected List[WidgetWithPos]

当然,我们可以放一堆# type:ignore,但这会使代码混乱,我相信有更聪明的方法;)

谢谢!

【问题讨论】:

    标签: python type-hinting mypy python-typing duck-typing


    【解决方案1】:

    这是 Alex Waygood 的 answer 的一个小变化,以删除 cast。诀窍是将@runtime_checkable 装饰器放在Protocol 类上。它只是让isinstance() 进行hasattr() 检查。

    import sys
    from dataclasses import dataclass
    from typing import List
    
    # Protocol has been added in Python 3.8+
    # so this makes the code backwards-compatible
    # without adding any dependencies
    # (typing_extensions is a MyPy dependency already)
    
    if sys.version_info >= (3, 8):
        from typing import Protocol, runtime_checkable
    else:
        from typing_extensions import Protocol, runtime_checkable
    
    
    @dataclass
    class Widget:
        """Generic class for widget"""
    
    
    @runtime_checkable
    class WithPos(Protocol):
        """Minimum interface of all widgets that have a position"""
        posx: int
        posy: int
    
    
    def some_function_that_does_something(widgets: List[Widget]):
        """A useful docstring that says what the function does"""
        widgets_with_pos = [w for w in widgets if isinstance(w, WithPos)]
    
        if not widgets_with_pos:
            raise AttributeError(f"No widget with position found among list {widgets}")
    
        first_widget = widgets_with_pos[0]
        pos_x = first_widget.posx
        pos_y = first_widget.posy
        print(f"Widget {first_widget} with position: {(pos_x, pos_y)}")
    

    以下代码(使用原问题中定义的其他子类)passes MyPy

    w1 = Group([])
    w2 = Rectangle(2, 3)
    some_function_that_does_something([w1, w2])
    

    进一步阅读

    作为参考,以下是 Alex 在他的回答中包含的一些链接:

    【讨论】:

    • 这真是个好主意。我没有想到使用 runtime_checkable 装饰器。
    • @AlexWaygood 我在阅读 PEP 544 时捡到了它。:)
    • 太棒了。让我们将其标记为已接受的答案,因为代码看起来更好。
    • 我支持将此作为接受的答案。代码在运行时更简洁、重复更少且更高效。
    【解决方案2】:

    我会使用typing.Protocoltyping.cast 来解决这个问题。 typing.Protocol 允许我们定义“structural types”——由 attributesproperties 定义的类型,而不是它们继承自的类——而typing.cast 是一个函数这在运行时没有任何影响,但允许我们向类型检查器断言对象具有特定类型。

    注意Protocol已经在Python 3.8中添加了所以对于3.7(3.6不支持dataclasses,虽然它也有一个backport),我们需要使用typing_extensions(这是mypy的一个依赖顺便说一句)。

    import sys
    from dataclasses import dataclass
    from typing import cast, List
    
    # Protocol has been added in Python 3.8+
    if sys.version_info >= (3, 8):
        from typing import Protocol
    else:
        from typing_extensions import Protocol
    
    
    @dataclass
    class Widget:
        """Generic class for widget"""
    
    
    class WidgetWithPosProto(Protocol):
        """Minimum interface of all widgets that have a position"""
        posx: int
        posy: int
    
    
    def some_function_that_does_something(widgets: List[Widget]):
        """A useful docstring that says what the function does"""
    
        widgets_with_pos = [
            cast(WidgetWithPosProto, w)
            for w in widgets
            if hasattr(w, "posx") and hasattr(w, "posy")
        ]
    
        if not widgets_with_pos:
            raise AttributeError(f"No widget with position found among list {widgets}")
    
        first_widget = widgets_with_pos[0]
        pos_x = first_widget.posx
        pos_y = first_widget.posy
        print(f"Widget {first_widget} with position: {(pos_x, pos_y)}")
    

    这个passes MyPy


    进一步阅读:

    【讨论】:

    • 谢谢@Alex:太棒了!顺便说一句,我们使用的是 Python 3.7,所以我更新了您的答案以提及我们需要的反向移植。将cast 放在元素级别而不是整个列表看起来会更好。
    • @Jean-FrancoisT。伟大的!感谢您关于何时引入 Protocol 的观点。我个人尝试尽可能少地使用cast,因为它确实引入了微小数量的运行时成本,即使它返回元素不变——这就是我在整体层面使用它的原因在我的原始答案中列出。但这只是个人喜好问题:)
    • 感谢您对运行时使用情况的评论
    • 这个答案对我很有帮助,因为除了在一个清晰完整的例子中展示了如何真正使用协议之外,它还让我阅读了 PEP 544。以前,我看过文档Protocol,没看懂,继续往下看。现在我已经在使用它了…… :)
    • 嘘,好吧,如果你还没有在我的脸上露出微笑@BenKovitz。非常感谢您的评论:)
    猜你喜欢
    • 2022-01-23
    • 1970-01-01
    • 2019-12-24
    • 2012-01-09
    • 2011-07-04
    • 1970-01-01
    • 2021-08-12
    • 2020-07-11
    相关资源
    最近更新 更多