如何向 mypy 指示 object 具有某些属性?
How to indicate to mypy an object has certain attributes?
我正在使用一些源自 parent class (Widget
) 的 classes;在 children 中,有些具有某些属性(posx
和 posy
),但有些则没有。
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
表明我们有一个具有给定属性的 object?
例如下面的函数和调用:
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)
会 return 预期的结果: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)
怎么办?
也许,一种方法是更改 classes 的设计:
Widget
的 class 与位置(例如 WidgetWithPos
)
Rectangle
和 Button
将派生自此 class
- 我们在函数中指出:
widget_with_pos: List[WidgetWithPos] = ...
...但是,我无法更改 classes 的原始设计并且 mypy
可能仍然会抱怨:
List comprehension has incompatible type List[Widget]; expected List[WidgetWithPos]
当然,我们可以放一堆 # type:ignore
但这会使代码混乱,我相信有更聪明的方法 ;)
谢谢!
我会使用 typing.Protocol
和 typing.cast
来解决这个问题。 typing.Protocol
允许我们定义“structural types”——由 attributes 或 properties 定义的类型,而不是 类 它们继承自 — 而 typing.cast
是一个在运行时没有效果的函数,但允许我们向类型检查器断言对象具有特定类型。
注意在Python3.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)}")
这个passes MyPy.
进一步阅读:
- Python documentation 对于
typing.Protocol
- PEP 544,介绍
typing.Protocol
并解释结构子类型的概念。
- MyPy documentation
Protocol
和结构子类型。
- Python documentation 对于
typing.cast
.
- MyPy documentation 对于
typing.cast
。
这是协议 class 上 Alex Waygood 的 , to remove the cast
. The trick is to put the @runtime_checkable 装饰器的一个小变体。它只是让 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)}")
以下代码(使用原问题中定义的其他子classes)passes MyPy:
w1 = Group([])
w2 = Rectangle(2, 3)
some_function_that_does_something([w1, w2])
进一步阅读
作为参考,以下是亚历克斯在他的回答中包含的一些链接:
- Python documentation 对于
typing.Protocol
- PEP 544,介绍
typing.Protocol
并解释结构子类型的概念。
- MyPy documentation 用于
Protocol
和结构子类型化。
我正在使用一些源自 parent class (Widget
) 的 classes;在 children 中,有些具有某些属性(posx
和 posy
),但有些则没有。
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
表明我们有一个具有给定属性的 object?
例如下面的函数和调用:
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)
会 return 预期的结果: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)
怎么办?
也许,一种方法是更改 classes 的设计:
Widget
的 class 与位置(例如WidgetWithPos
)Rectangle
和Button
将派生自此 class- 我们在函数中指出:
widget_with_pos: List[WidgetWithPos] = ...
...但是,我无法更改 classes 的原始设计并且 mypy
可能仍然会抱怨:
List comprehension has incompatible type List[Widget]; expected List[WidgetWithPos]
当然,我们可以放一堆 # type:ignore
但这会使代码混乱,我相信有更聪明的方法 ;)
谢谢!
我会使用 typing.Protocol
和 typing.cast
来解决这个问题。 typing.Protocol
允许我们定义“structural types”——由 attributes 或 properties 定义的类型,而不是 类 它们继承自 — 而 typing.cast
是一个在运行时没有效果的函数,但允许我们向类型检查器断言对象具有特定类型。
注意在Python3.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)}")
这个passes MyPy.
进一步阅读:
- Python documentation 对于
typing.Protocol
- PEP 544,介绍
typing.Protocol
并解释结构子类型的概念。 - MyPy documentation
Protocol
和结构子类型。 - Python documentation 对于
typing.cast
. - MyPy documentation 对于
typing.cast
。
这是协议 class 上 Alex Waygood 的 cast
. The trick is to put the @runtime_checkable 装饰器的一个小变体。它只是让 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)}")
以下代码(使用原问题中定义的其他子classes)passes MyPy:
w1 = Group([])
w2 = Rectangle(2, 3)
some_function_that_does_something([w1, w2])
进一步阅读
作为参考,以下是亚历克斯在他的回答中包含的一些链接:
- Python documentation 对于
typing.Protocol
- PEP 544,介绍
typing.Protocol
并解释结构子类型的概念。 - MyPy documentation 用于
Protocol
和结构子类型化。