在 Django 模型中使用协议引发元类冲突错误

Using Protocols in django models raises metaclass conflict error

假设我有 PEP-544 协议称为 Summable :

class Summable(Protocol):
    @property
    total_amount()-> Decimal:
      ...

我有模型 Item 实现了 Protocol

class Item(Summable, models.Model):
    discount = models.DecimalField(
        decimal_places=2,
        validators=[MaxValueValidator(1)],
        default=Decimal('0.00'),
        max_digits=10
    )
    price = models.DecimalField(
        decimal_places=4,
        validators=[MinValueValidator(0)],
        max_digits=10
    )

    @property
    def total_amount(self) - > Decimal:
       return self.price - self.price * self.discount

    class Meta:
        ordering = ['id']

我得到:

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

即使我也从 Summable.Meta 和 models.Model.Meta 扩展 Item 的 Meta,也会发生同样的情况。

我正在使用 python 3.9 有什么想法吗?

好吧,那里有很多陷阱:

  1. 您需要创建一个新元class:

例如:

class ModelProtocolMeta(type(Model), type(Protocol)):
     pass
  1. 您需要将协议放在最后,这样协议就不会用 no_init 覆盖模型的构造函数。 协议的no_init构造函数如下:
def _no_init(self, *args, **kwargs):
    if type(self)._is_protocol:
        raise TypeError('Protocols cannot be instantiated')

所以它会默默地覆盖构造函数而不会出现任何错误,因为继承的 class 会将 _is_protocol 设置为 False

(注意没有调用super,所以我们说的是完全覆盖)

所以在一天结束时我们需要以下内容:

class Item(models.Model, Summable, metaclass=ModelProtocolMeta):
    discount = models.DecimalField(
        decimal_places=2,
        validators=[MaxValueValidator(1)],
        default=Decimal('0.00'),
        max_digits=10
    )
    price = models.DecimalField(
        decimal_places=4,
        validators=[MinValueValidator(0)],
        max_digits=10
    )

    @property
    def total_amount(self) -> Decimal:
       return sel.price - self.price * self.discount

这与 Andreas 所做的类似,但我认为更容易使用:

from typing import Protocol
from django.db import models
from typing import Any

django_model_type: Any = type(models.Model)
protocol_type: Any = type(Protocol)

class ModelProtocolMeta(django_model_type, protocol_type):
    """
    This technique allows us to use Protocol with Django models without metaclass conflict
    """
    pass

class Summable(Protocol):
    def total_amount(self) -> int: ...

class SummableModel(Summable, metaclass=ModelProtocolMeta): ...

并使用它:

class Money(models.Model, SummableModel):
    def total_amount(self) -> int:
        return 100_000_000