简体   繁体   English

如何向 mypy 指示 object 具有某些属性?

[英]How to indicate to mypy an object has certain attributes?

I am using some classes that derived from a parent class ( Widget );我正在使用一些派生自父 class ( Widget ) 的类; among the children, some have certain attributes ( posx and posy ) but some don't.在孩子中,有些具有某些属性( 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"

Even after doing some filtering with only widgets with these attributes, mypy is not able to recognize that they should have.即使在仅对具有这些属性的小部件进行一些过滤之后, mypy也无法识别出它们应该具有的属性。

Is there a way to indicate to mypy that we have an object with a given attribute?有没有办法向mypy表明我们有一个具有给定属性的 object?

For example, the following function and call:例如下面的function并拨打:

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)

would return a result as expected: Widget Button(posx=10, posy=10, width=200, height=100, label='A button') with position: (10, 10)将按预期返回结果: Widget Button(posx=10, posy=10, width=200, height=100, label='A button') with position: (10, 10)

But mypy would complain:但是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)

How to do?怎么做?

Maybe, one way could be to change the design of the classes:也许,一种方法是改变类的设计:

  • Subclass of Widget with the position (eg WidgetWithPos )带有 position 的Widget的子类(例如WidgetWithPos
  • Rectangle and Button would derive from this class RectangleButton将从这个 class 派生
  • We indicate in our function: widget_with_pos: List[WidgetWithPos] =...我们在我们的 function 中指出: widget_with_pos: List[WidgetWithPos] =...

... however, I cannot change the original design of the classes and mypy might still complain with something like: ...但是,我无法更改类的原始设计, mypy可能仍会抱怨类似以下内容:

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

Of course, we could put a bunch of # type:ignore but that will clutter the code and I am sure there is a smarter way;)当然,我们可以放一堆#type # type:ignore ,但这会使代码混乱,我相信有更聪明的方法;)

Thanks!谢谢!

I would use typing.Protocol and typing.cast to solve this.我会使用typing.Protocoltyping.cast来解决这个问题。 typing.Protocol allows us to define " structural types " — types that are defined by attributes or properties rather than the classes they inherit from — and typing.cast is a function that has no effect at runtime, but allows us to assert to the type-checker that an object has a certain type. typing.Protocol允许我们定义“ 结构类型”——由属性属性定义的类型,而不是它们继承自的类——而typing.cast是一个 function,它在运行时没有效果,但允许我们断言类型- 检查 object 是否具有特定类型。

Note that Protocol has been added in Python 3.8 so for 3.7 (3.6 does not support dataclasses , although it also has a backport), we need to use typing_extensions (which is a dependency of mypy by the way).请注意,Python 3.8 中已添加Protocol ,因此对于 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)}")

This passes MyPy .通过了 MyPy


Further reading:延伸阅读:

Here's a small variation on Alex Waygood's answer , to remove the cast .这是 Alex Waygood 的答案的一个小变体,用于删除cast表。 The trick is to put the @runtime_checkable decorator on the Protocol class. It simply makes isinstance() do the hasattr() checks.诀窍是将@runtime_checkable装饰器放在协议 class 上。它只是让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)}")

The following code (using the other sub-classes defined in the original question) passes MyPy :以下代码(使用原始问题中定义的其他子类) 通过 MyPy

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

Further reading延伸阅读

For reference, here are some of the links Alex included in his answer:作为参考,以下是亚历克斯在他的回答中包含的一些链接:

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

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