如何子类化 Django TextChoices 以添加其他属性?
Posted
技术标签:
【中文标题】如何子类化 Django TextChoices 以添加其他属性?【英文标题】:How can I subclass Django TextChoices to add additional attributes? 【发布时间】:2021-01-16 15:52:08 【问题描述】:我想将Django 3.0 TextChoices 用作models.CharField
choices option 并在我的代码中的其他地方用作Enum
。
这是我的代码的简化版本:
from django.db import models
class ValueTypeOriginal(models.TextChoices):
# I know Django will add the label for me, I am just being explicit
BOOLEAN = 'boolean', 'Boolean'
class Template(models.Model):
value_type = models.CharField(choices=ValueTypeOriginal.choices)
我想为枚举成员添加一个附加属性,以便调用
>>> ValueType.BOOLEAN.native_type
bool
有效。
bool
这里不是字符串,而是built-in python function。
This blog post 描述了通过覆盖__new__
来使用Enum
做类似的事情。
class Direction(Enum):
left = 37, (-1, 0)
up = 38, (0, -1)
right = 39, (1, 0)
down = 40, (0, 1)
def __new__(cls, keycode, vector):
obj = object.__new__(cls)
obj._value_ = keycode
obj.vector = vector
return obj
基于我尝试过的:
class ValueTypeModified(models.TextChoices):
BOOLEAN = ('boolean', bool), 'Boolean'
def __new__(cls, value):
obj = str.__new__(cls, value)
obj._value_, obj.native_type = value
return obj
这几乎行得通。我可以访问独特的TextChoices
属性,如.choices
,并且我有属性.native_type
,但字符串比较不能正常工作。
>>> ValueTypeOriginal.BOOLEAN == 'boolean'
True
>>> ValueTypeModified.BOOLEAN == 'boolean'
False
我想我误解了__new__
方法,但我不知道我应该做些什么不同。
更新
为了回应 Ethan Furman 的回答,我尝试了
class ValueType(TextChoices):
BOOLEAN = (('boolean', bool), 'Boolean')
def __new__(cls, value, type):
obj = str.__new__(value)
obj._value_ = value
obj.native_type = type
return obj
但得到
TypeError: __new__() missing 1 required positional argument: 'type'
所以我又去了
class ValueType(TextChoices):
BOOLEAN = (('boolean', bool), 'Boolean')
def __new__(cls, value):
obj = str.__new__(value)
obj._value_ = value[0]
obj.native_type = value[1]
return obj
但我明白了:
TypeError: str.__new__(X): X is not a type object (tuple)
那么
class ValueType(TextChoices):
BOOLEAN = (('boolean', bool), 'Boolean')
def __new__(cls, value):
obj = str.__new__(cls)
obj._value_ = value[0]
obj.native_type = value[1]
return obj
让我回到我开始直接字符串比较失败的地方
>>> ValueTypeOriginal.BOOLEAN == 'boolean'
True
>>> ValueType.BOOLEAN == 'boolean'
False
然而,
>>> ValueType.BOOLEAN.value == 'boolean'
True
所以正确的值似乎到达了那里,但枚举成员本身的评估不像ValueType(str, Enum)
,而是在比较时像ValueType(Enum)
。
更新 #2
我现在已经试过了:
class ValueType(TextChoices):
BOOLEAN = (('boolean', bool), 'Boolean')
def __new__(cls, value):
obj = str.__new__(cls, value)
obj._value_ = value[0]
obj.native_type = value[1]
return obj
class ValueType(str, Choices):
BOOLEAN = (('boolean', bool), 'Boolean')
def __new__(cls, value):
obj = str.__new__(cls)
obj._value_ = value[0]
obj.native_type = value[1]
return obj
为了安全起见
class ValueType(TextChoices):
BOOLEAN = (('boolean', bool), 'Boolean')
def __new__(cls, value):
obj = super().__new__(cls, value)
obj._value_ = value[0]
obj.native_type = value[1]
return obj
但没有一个按预期给我直接的字符串比较。
更新 #3 我终于明白 Ethan Furman 告诉我要做什么了。
解决方案:
class ValueType(TextChoices):
BOOLEAN = (('boolean', bool), 'Boolean')
def __new__(cls, value):
obj = str.__new__(cls, value[0])
obj._value_ = value[0]
obj.native_type = value[1]
return obj
【问题讨论】:
【参考方案1】:你快到了。部分困难只是两个参数中的第一个,即元组 ('boolean', bool)
,被传递给 Enum
机器。
所以,我们有两个选择:
保持元组原样并使用索引访问(您当前的工作解决方案):
def __new__(cls, value): # value[0] is 'boolean'; value[1] is bool
在__new__
标头中命名参数:
def __new__(cls, svalue, type): # value is split into named arguments
请注意,我稍微更改了名称,希望有助于避免混淆。
把它们放在一起,你的最终方法应该是这样的(使用上面的第二个选项):
def __new__(cls, svalue, type):
obj = str.__new__(cls, svalue)
obj._value_ = svalue
obj.native_type = type
return obj
注意:
__new__
的第一个参数是您尝试创建的实例的类——通常与定义 __new__
方法的类相同。即使看起来不像,__new__
是一个classmethod
——它只是特殊情况下不需要classmethod
装饰器。
【讨论】:
我正在尝试将您的建议映射到我的代码中。由于您提供的特定代码混合了博客文章中的示例 (vector
) 和我的实际代码 (type
),我认为我在翻译中丢失了一些东西。
具体来说,当我通过 obj = str.__new__(value)
时,我得到一个 TypeError: str.__new__(X): X is not a type object (tuple)
我已对我的问题添加了更新,希望能解释我遇到的情况,如果您愿意再看一遍。感谢您在这方面帮助我。
@kcontr:啊,我忘了str.__new__(cls, value)
中的cls
部分。试试看。
我还在三振。请参阅更新 #2 了解我对各种组合的尝试。【参考方案2】:
这是我使用一些元类魔法所做的:
class Mwahaha(type(models.TextChoices)):
def __new__(metacls, classname, bases, classdict):
native_types = member: classdict[member][1] for member in classdict._member_names
classdict._member_names.clear()
for member in native_types.keys():
val = classdict[member][0]
del classdict[member]
classdict[member] = val
cls = super().__new__(metacls, classname, bases, classdict)
for member, n_t in native_types.items():
getattr(o, member).native_type = n_t
return cls
让你的班级看起来像
class ValueTypeModified(models.TextChoices, metaclass=Mwahaha):
BOOLEAN = ('boolean', 'Boolean'), bool
clear
和 del
是绕过某些 Enum._dict
保护措施所必需的,以防止覆盖枚举或属性。然而,这正是我们想要在这里做的。
不使用metaclass
可能有一种更简单的方法可以做到这一点,但我已经做好了准备并准备走那条路¯\_(ツ)_/¯
【讨论】:
【参考方案3】:我发布了我自己的答案,以便我可以解释我所学到的知识,这要感谢@Ethan Furman 的出色帮助!
从您的代码看来,
value
是('boolean', bool)
,所以当 你会的obj = str.__new__(cls, value)
obj
最终成为"('boolean', bool)"
这意味着这将起作用,即使这不是我的意图
>>> ValueType.BOOLEAN == str(('boolean', bool))
True
同样,如果我根本不将value
传递给str.__new__
构造函数(即str.__new__(cls)
),那么obj
最终会成为空字符串''
,就像调用str()
而没有论据。
这意味着这会起作用,即使这不是我的意图:
>>> ValueTypeEmptyString.BOOLEAN == ''
True
最后真的是我对__new__
dunder method的误解。因为我正在做一个str.__new__
调用而不仅仅是一个通用的object.__new__
调用,所以第一个参数应该是str
本身或str
的子类。在我的例子中,TextChoices
是 str 的子类,所以 ValueType
也是 str
的子类,并且可以是 str.__new__
方法的第一个参数。
然后,正如docs for __new__
解释的那样,
其余参数是传递给对象构造函数表达式(对类的调用)的参数。
或者换句话说,我可以认为剩余的参数直接输入到str()
调用中。由于我不想对整个元组进行字符串化,而只是对该元组的第一个元素进行字符串化,因此我应该只将第一个元素传递给 str.__new__
调用。
所以把它们放在一起:
class ValueType(TextChoices):
BOOLEAN = (('boolean', bool), 'Boolean')
def __new__(cls, value):
# cls is <enum 'ValueType'>, a subtype of str, while value[0] is `'boolean'`
obj = str.__new__(cls, value[0])
# value[0] is again `'boolean'`
obj._value_ = value[0]
# value[1] is `bool`
obj.native_type = value[1]
return obj
ChoicesMeta
元类处理在外部元组中添加传递的Boolean
标签以及.choices
的其他元类魔法的方法对我来说并不完全清楚,但现在我在至少有我正在寻找的“工作代码”和
>>> ValueType.BOOLEAN == 'boolean'
True
有效。
【讨论】:
以上是关于如何子类化 Django TextChoices 以添加其他属性?的主要内容,如果未能解决你的问题,请参考以下文章
Django子类化multiwidget - 使用自定义multiwidget重建帖子日期
我可以将 elasticsearch-dsl 的 IpRange 子类化以供 django-elasticsearch-dsl 使用吗?