在 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
有什么想法吗?
好吧,那里有很多陷阱:
- 您需要创建一个新元class:
例如:
class ModelProtocolMeta(type(Model), type(Protocol)):
pass
- 您需要将协议放在最后,这样协议就不会用 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
假设我有 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
有什么想法吗?
好吧,那里有很多陷阱:
- 您需要创建一个新元class:
例如:
class ModelProtocolMeta(type(Model), type(Protocol)):
pass
- 您需要将协议放在最后,这样协议就不会用 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