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

Posted

技术标签:

【中文标题】如何正确地将类型提示添加到 Mixin 类?【英文标题】:How do I correctly add type-hints to Mixin classes? 【发布时间】:2019-01-26 13:25:37 【问题描述】:

考虑以下示例。该示例是人为设计的,但在一个可运行的示例中说明了这一点:

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中缺少属性错误?” @JonathonReinhart 我不明白你的意思。如果我删除类型提示,那么mypy 将不再做任何事情(假设所有内容都是Any 类型)。所以我看不出这样做的意义。 FWIW,我删除了类型提示并再次运行它,正如预期的那样,错误消失了(因为一切都是Any)。 对不起,我不熟悉 mypy,并认为它只是一个 pylint 样式的检查器。不过,我觉得这与类型提示本身没有任何关系,只是 mypy 工具的一个限制。 绝对有可能。但在那种情况下,最好了解这种情况下的任何最佳实践。我可以撒一些# type: ignore cmets,但我想看看在完全禁用类型检查之前是否有替代方法。 【参考方案1】:

作为参考,mypy 建议通过 Protocol(文档 here)实现 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 包中提供。

【讨论】:

是的。我昨天看到了公告。这真的是一个很好的补充。不能等到我们的服务器支持这个!【参考方案2】:

我在this question 中看到的一种方法是类型提示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

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

【讨论】:

这不会破坏 mixin 的目的吗?因为现在您只能将它与MyBaseClass 的子类一起使用。这意味着您也可以将 add 方法移动到 MyBaseClass 就我而言,我正在为 django-rest-framework 构建 mixins,它还附带了几个 mixins:github.com/encode/django-rest-framework/blob/master/… mixins 总是与 GenericAPIView 一起使用,给出基本功能,而每个 mixin 提供不同的附加功能 简化:MyBaseClass 可以提供各种计算,但用户应该选择支持哪些。 MyBaseClass 提供了一个value 属性,用户可以选择他们在应用程序中需要的AdditionMixinSubtractionMixinDivisionMixinMultiplicationMixin 中的哪一个 @exhuma no:如果一个 mixin 声明了一些关于它想要混入的类的期望,这不是问题 :) 不过,协议更好地服务于这个目的【参考方案3】:

我已经在我的机器上测试过了,希望它也对你有用:

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

【讨论】:

MyClass 有很多属性时,我最终会复制所有属性,特别是如果MyClass 来自外部依赖项时,我会遇到维护问题。我不认为这是一个可行的解决方案。 我实际上认为这是正确的解决方案。您只需要为 mixin 所关注的值添加类级别的属性。这很有意义。 @soerface 很好,这取决于。对于我的用例,Mixin 可以被多个超类使用以获得一些额外的功能。如果你使用多个Mixin 只支持一个超类,这不适合你。 这个解决方案在 ORM 模型中不是有问题吗?添加类变量会导致 ORM 中的新属性吗?【参考方案4】:

尝试:

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

【讨论】:

这似乎过于复杂只是以获得类型提示。 是的,但也很聪明。只要没有官方实施,这似乎是我的首选方式。供参考here the link mypy 问题中的相同提案。【参考方案5】:

除了坎皮关于the mypy's recommendation of typing mixins with Protocol的回答:

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

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

【讨论】:

这感觉比我目前接受的答案要好得多,也更符合当前的状态。感谢更新。注意:我认为您的意思是“Campi”而不是“Dec”;) @exhuma 哈哈。我混淆了月份和用户名;)【参考方案6】:

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

Guido van Rossum 本人提出的here:

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)

【讨论】:

@exhuma:这是一个补充答案,它不仅涵盖变量,而且是处理可调用对象的一种方式。它还包含有关 Python 的创建者自己的想法的有用信息。如果 Sraw 会用这些信息更新他的答案(这对我很有用,因为我没有找到我想要的东西)或者其他人会采取主动(我不确定它是否允许),我'我愿意删除我的。 请不要误会,我主要担心我遗漏了一些东西,也许我可以从你的回答中学到一些东西:我仍然看不出有什么不同。我能弄清楚的唯一区别是它使用注释而不是# type: ... 注释。使用assertLess = None # type: Callable 应该是一样的。还是我错过了什么? @exhauma: AFAIK assertLess = None # type: Callable 是 Python 2 执行此操作的方式,因为仅在 Python 3.6 中引入了真正的类型。因此,现在显式使用类型会更好。此外,我在回答中提到的链接中发现的对我来说最有价值的是 van Guido van Rossum 本人的方法。对我来说,他在 GitHub 问题上的回答是最好的。 :) 类型注释也适用于 Python foo = None # type: Optional[Callable] 以适应默认值。 这个答案建议使用与@Sraw 之前的答案相同的原则,即直接向 mixin 属性添加类型提示。它引入了维护成本,以使每个 mixin 的类型提示与其潜在的基类保持最新。【参考方案7】:

我的解决方案:在 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))

【讨论】:

【参考方案8】:

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

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 类?的主要内容,如果未能解决你的问题,请参考以下文章

类型为“type”的枚举类作为mixin

如何正确地将对象 $push 到 MongoDB 模式属性中,类型:数组

正确地将字符串添加到 Windows 注册表中的 REG_BINARY 类型

如何正确地将值分配给数组(结构类型)元素?

正确地将 info.plist 条目转换为 NS* 类型

检索所有包含特定 mixin 的类定义的正确方法是啥?