如何正确地向 Mixin 类添加类型提示?



请考虑以下示例。该示例是人为的,但说明了可运行示例中的要点:

class MultiplicatorMixin:
def multiply(self, m: int) -> int:
return self.value * m

class AdditionMixin:
def add(self, b: int) -> int:
return self.value + b

class MyClass(MultiplicatorMixin, AdditionMixin):
def __init__(self, value: int) -> None:
self.value = value

instance = MyClass(10)
print(instance.add(2))
print(instance.multiply(2))

执行时,这将给出以下输出:

12
20

代码有效。

但是在其上运行mypy会产生以下错误:

example.py:4: error: "MultiplicatorMixin" has no attribute "value"
example.py:10: error: "AdditionMixin" has no attribute "value"

我明白为什么mypy会给出这个结果。但是mixin类从不单独使用。它们始终用作附加超类。

对于上下文,这是一种已在现有应用程序中使用的模式,我正在添加类型提示。在这种情况下,错误是误报。我正在考虑使用 mixins 重写部分,因为我不是特别喜欢它,重组类层次结构可能也是如此。

但我仍然想知道这样的事情如何被恰当地暗示。

作为参考,mypy 建议通过Protocol实现 mixins(文档在这里(。

它适用于 mypy>= 750。

from typing import Protocol

class HasValueProtocol(Protocol):
@property
def value(self) -> int: ...

class MultiplicationMixin:
def multiply(self: HasValueProtocol, m: int) -> int:
return self.value * m

class AdditionMixin:
def add(self: HasValueProtocol, b: int) -> int:
return self.value + b

class MyClass(MultiplicationMixin, AdditionMixin):
def __init__(self, value: int) -> None:
self.value = value

Protocol基类在 Python 2.7 和 3.4-3.7 的typing_extensions包中提供。

除了坎皮关于mypy建议用Protocol输入mixins的回答:

键入方法self的替代方法是继承协议。

from typing import Protocol

class HasValueProtocol(Protocol):
@property
def value(self) -> int: ...

class MultiplicationMixin(HasValueProtocol):
def multiply(self, m: int) -> int:
return self.value * m

class AdditionMixin(HasValueProtocol):
def add(self, b: int) -> int:
return self.value + b

class MyClass(MultiplicationMixin, AdditionMixin):
def __init__(self, value: int) -> None:
self.value = value

此外,如果您TYPE_CHECKINGProtocol,并且鉴于您无法转发引用父类(即将父类作为字符串文本传递(,解决方法是:

from typing import Protocol, TYPE_CHECKING

if TYPE_CHECKING:
class HasValueProtocol(Protocol):
@property
def value(self) -> int: ...
else:
class HasValueProtocol: ...

class MultiplicationMixin(HasValueProtocol):
def multiply(self, m: int) -> int:
return self.value * m
...

我在这个问题中看到的一种方法是类型暗示self属性。与类型包中的Union一起,您可以使用与 mixin 一起使用的类中的属性,同时仍然对自己的属性具有正确的类型提示:

from typing import Union
class AdditionMixin:
def add(self: Union[MyBaseClass, 'AdditionMixin'], b: int) -> int:
return self.value + b

class MyBaseClass:
def __init__(self, value: int):
self.value = value

缺点是您必须向每种方法添加提示,这有点麻烦。

尝试:

from typing import Type, TYPE_CHECKING, TypeVar
T = TypeVar('T')

def with_typehint(baseclass: Type[T]) -> Type[T]:
"""
Useful function to make mixins with baseclass typehint
```
class ReadonlyMixin(with_typehint(BaseAdmin))):
...
```
"""
if TYPE_CHECKING:
return baseclass
return object

在 Pyright 中测试的示例:

class ReadOnlyInlineMixin(with_typehint(BaseModelAdmin)):
def get_readonly_fields(self,
request: WSGIRequest,
obj: Optional[Model] = None) -> List[str]:
if self.readonly_fields is None:
readonly_fields = []
else:
readonly_fields = self.readonly_fields # self get is typed by baseclass
return self._get_readonly_fields(request, obj) + list(readonly_fields)
def has_change_permission(self,
request: WSGIRequest,
obj: Optional[Model] = None) -> bool:
return (
request.method in ['GET', 'HEAD']
and super().has_change_permission(request, obj) # super is typed by baseclass
)
>>> ReadOnlyAdminMixin.__mro__
(<class 'custom.django.admin.mixins.ReadOnlyAdminMixin'>, <class 'object'>)

我已经在我的机器上测试过它,希望它也能为你工作:

class MultiplicatorMixin:
value = None # type: int
def multiply(self, m: int) -> int:
return self.value * m

class AdditionMixin:
value = None # type: int
def add(self, b: int) -> int:
return self.value + b

class MyClass(MultiplicatorMixin, AdditionMixin):
def __init__(self, value: int) -> None:
self.value = value

instance = MyClass(10)
print(instance.add(2))
print(instance.multiply(2))

我的解决方案:添加一个没有任何 init 的value: int到 Mixin 类:

class MultiplicatorMixin:
value: int
def multiply(self, m: int) -> int:
return self.value * m

class AdditionMixin:
value: int
def add(self, b: int) -> int:
return self.value + b

class MyClass(MultiplicatorMixin, AdditionMixin):
def __init__(self, value: int) -> None:
self.value = value

instance = MyClass(10)
print(instance.add(2))
print(instance.multiply(2))

除了上面提到的好答案。我的用例 - 混合用于测试。

正如Guido van Rossum本人在这里提议的那样:

from typing import *
T = TypeVar('T')
class Base:
fit: Callable
class Foo(Base):
def fit(self, arg1: int) -> Optional[str]:
pass
class Bar(Foo):
def fit(self, arg1: float) -> str:
pass    

因此,当涉及到混合时,它可能如下所示:


class UsefulMixin:
assertLess: Callable
assertIn: Callable
assertIsNotNone: Callable
def something_useful(self, key, value):
self.assertIsNotNone(key)
self.assertLess(key, 10)
self.assertIn(value, ['Alice', 'in', 'Wonderland']

class AnotherUsefulMixin:
assertTrue: Callable
assertFalse: Callable
assertIsNone: Callable
def something_else_useful(self, val, foo, bar):
self.assertTrue(val)
self.assertFalse(foo)
self.assertIsNone(bar)  

我们的最后一堂课如下所示:

class TestSomething(unittest.TestCase, UsefulMixin, AnotherUsefulMixin):
def test_something(self):
self.something_useful(10, 'Alice')
self.something_else_useful(True, False, None)

一个不必编写的方法 类型提示每个方法:

import typing

class FooMixin:
base = typing.Union["Hello", "World"]
def alpha(self: base):
self.hello()
def beta(self: base):
self.world()

class Base(object):
pass

class Hello(Base, FooMixin):
def hello(self):
print("hello from", self)

class World(Base, FooMixin):
def world(self):
print("world from", self)

Hello().alpha()
World().beta()

在mixin中使用ProtocolBaseClass时,可以做这样的事情:

from typing import TYPE_CHECKING, Protocol, cast

class BaseClass:
def __init__(self) -> None:
self.name = "base name"

class SizeProtocol(Protocol):
size: int

class DoubleSizeMixin:
"""
Add this mixin to classes implementing `SizeProtocol` and inheriting from `BaseClass`
"""
def get_double_size_with_name(self) -> tuple:
_self: "DoubleSizeMixinT" = self  # type: ignore
return (_self.name, _self.size * 2)
# Another option:
def get_double_size_with_name_v2(self) -> tuple:
self = cast("DoubleSizeMixinT", self)  # pylint: disable=self-cls-assignment
return (self.name, self.size * 2)

if TYPE_CHECKING:
class DoubleSizeMixinT(SizeProtocol, DoubleSizeMixin, BaseClass):
pass

class A(BaseClass, SizeProtocol, DoubleSizeMixin):
def __init__(self) -> None:
super().__init__()
self.size = 2

print(A().get_double_size_with_name())

最新更新