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

59

考虑以下示例。这个例子是人为的,但通过可运行的示例说明了要点:

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错误的原因吗?还是即使没有类型提示,您仍然会遇到这些错误?如果是这种情况,那么类型提示与问题无关,我认为您的问题应该是“如何处理mypy中缺少属性错误?” - Jonathon Reinhart
@JonathonReinhart 我不明白你的意思。如果我删除类型提示,那么 mypy 将不再起作用(假设一切都是 Any 类型)。所以我不明白这样做的意义。顺便说一下,我删除了类型提示并再次运行它,正如预期的那样,错误已经消失了(因为一切都是 Any)。 - exhuma
抱歉,我不熟悉mypy,并且认为它只是一个类似于pylint的检查器。尽管如此,我觉得这与类型提示本身没有什么关系,只是mypy工具的限制。 - Jonathon Reinhart
完全有可能。但在这种情况下,最好知道任何最佳实践。我可以在周围添加一些# type: ignore注释,但在完全禁用类型检查之前,我想看看是否有其他选择。 - exhuma
9个回答

61

供参考,mypy建议通过Protocol实现混合(Mixin)(文档在这里)。

它适用于 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 基类提供在 typing_extensions 包中,适用于 Python 2.7 和 3.4-3.7。


1
是的,我昨天看到了这个公告。这真的是一个很好的补充。迫不及待地想要我们的服务器支持它! - exhuma
使用self: HasValueProtocolAdditionMixin方法中的一个很大的缺点是,self不再是AdditionMixin类型。这意味着在这些方法的内部调用AdditionMixin中定义的其他方法将会出错。 - undefined

22

除了Campi的答案关于mypy建议使用Protocol进行类型混合之外:

另一种方法是直接继承协议来代替对方法中的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_CHECKING 一个 Protocol,鉴于您无法转发引用父类(即将父类作为字符串文字传递),解决方法如下:
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

...

2
这感觉比我目前接受的答案好得多,而且更符合当前状态。感谢更新。注意:我想你可能是指“Campi”而不是“Dec” ;) - exhuma
5
这个答案比输入单独的“self”参数要好得多,因为将self作为协议来输入可以防止你访问Mixin类本身的属性。(它只是协议) - Michael Scott Asato Cuthbert
1
@TuukkaMustonen 不行。一个类需要显式地继承Protocol才能成为协议(class MultiplicationMixin(HasValueProtocol, Protocol): ...)。否则,它只会继承协议的默认实现。而且,在任何情况下,协议都可以包含实际代码。 - Nuno André
1
@MattLyon 在 gist 中回答了。 - Nuno André
2
Campi的答案如果您的mixin定义了需要相互访问的多个方法(因为您正在使用字段注释self,而不再是mixin本身),则会失败。 这个答案将有效,并且您还可以从任何其他所需类型继承,而协议只能从协议继承。 - theberzi
显示剩余13条评论

12

我在这个问题中看到的一种方法是对self属性进行类型提示。与typing包中的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

缺点是必须在每个方法中添加提示,这有点麻烦。


7
这样做似乎会违背mixin的初衷吧?因为现在你只能将它用于MyBaseClass 的子类中。这意味着你也可以把 add 方法移到 MyBaseClass 中。 - exhuma
3
在我的情况下,我正在为Django Rest Framework构建mixins,它还带有一些mixins: https://github.com/encode/django-rest-framework/blob/master/rest_framework/mixins.py 这些mixins始终与[GenericAPIView](https://github.com/encode/django-rest-framework/blob/master/rest_framework/generics.py#L26)一起使用,提供基本功能,而每个mixin提供不同的附加功能。 - soerface
3
MyBaseClass可以提供各种计算功能,但用户应该选择哪些功能需要支持。MyBaseClass提供了一个value属性,用户可以选择他们在应用中需要使用的混入类AdditionMixinSubtractionMixinDivisionMixinMultiplicationMixin - soerface
1
@exhuma 不,如果一个 mixin 声明了一些关于它想要混入的类的期望,这并不是问题 :) 不过,协议更好地实现了这个目的。 - kolypto

8

尝试使用:

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'>)

4
为了获得类型提示,这似乎过于复杂了。 - exhuma
1
是的,但也有点聪明。只要没有官方实现,这似乎是我首选的方式。供参考,这里是链接,同样的提案在 mypy 的问题中讨论过。 - Kound
在像Qt这样使用在C中定义的自定义元类的框架中,为混合类(mixin classes)提供这样的功能是必要的。在这种情况下,从多个Qt基类进行多重继承是无效的。 - undefined

4

我已在我的机器上进行了测试,希望它也适用于您的机器:

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))

22
MyClass有很多属性时,我会不得不复制它们所有,这将导致维护困难,尤其是如果MyClass来自外部依赖项。我认为这不是一个可行的解决方案。 - soerface
1
我认为这是正确的解决方案。您只需要为与mixin相关的值添加类级别的属性。这非常合理。 - exhuma
1
@soerface 好的,这要看情况。在我的使用场景中,Mixin 可以被多个超类使用以获得一些额外的功能。如果你只使用多个 Mixin 来支持一个超类,那这对你来说可能不适用。 - Sraw
这个解决方案在ORM模型中是否有问题?添加一个类变量会不会导致ORM中出现新的属性? - Martin Thoma

4
我的解决方案:在Mixin类中添加一个value: int属性,不需要任何初始化。
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))

我在想,既然这个解决方案没有得到很多赞,那么应该会有什么问题。但是我可以看到,它只需要添加最少的代码就可以正常工作? - Salma Hassan

2

除了上面提到的好答案外,我的使用情况 - mixins 要用于测试。

正如 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    

因此,当涉及到mixin时,它可能看起来像这样:

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)

2
@exhuma:这是一个补充回答,不仅涵盖了变量,还包括了处理可调用对象的方法。它还包含了Python创始人对此的看法,这些信息非常有用。如果Sraw能够更新他的答案并加入这些信息(对我来说非常有用,因为我没有找到我需要的东西),或者其他人愿意采取这个举措(我不确定是否允许),我愿意删除我的回答。 - Artur Barseghyan
请不要误解我的意思,我主要担心自己漏掉了什么,也许可以从你的回答中学到一些东西:我仍然看不出区别。我唯一能看出来的区别是它使用注释而不是 # type: ... 注释。使用 assertLess = None # type: Callable 应该是相同的。或者我漏掉了什么? - exhuma
据我所知,“assertLess = None # type: Callable”是Python 2的做法,因为真正的类型提示只在Python 3.6中引入。因此,现在明确地使用类型提示更好。此外,在我提到的链接中,我发现最有价值的是Guido van Rossum本人的方法。对我来说,他在GitHub问题中的回答是最好的。 :) - Artur Barseghyan
Type-comment 在 Python 小于 3.6 的版本中也是有效的。虽然这种语法是在 3.6 中引入的,但是类型提示(或者更确切地说是“注释”)自 3.3 以来就一直存在了。我经常对 Python 3.5 代码进行类型检查,因此仍然依赖于 type-comment。我同意新的 3.6 语法更加简洁,不过从技术上讲,在旧的语法中需要编写 foo = None # type: Optional[Callable]来适应默认值。 - exhuma
这个答案建议使用与@Sraw早期答案相同的原则,即直接向mixin属性添加类型提示。它引入了一项维护成本,即保持每个mixin的类型提示与其潜在基类的最新状态。 - Peter Bašista

1

一种方法是不必在每个方法中编写类型提示:

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()

0

当使用协议基类混合时,可以像这样做:

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())

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接