如何子类化 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

cleardel 是绕过某些 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 表单字段不再是必需的?

子类化 Django ListView

Django子类化multiwidget - 使用自定义multiwidget重建帖子日期

我可以将 elasticsearch-dsl 的 IpRange 子类化以供 django-elasticsearch-dsl 使用吗?

Django 模型子类化方法

你如何在 django 中扩展站点模型?