如何子类化 Django TextChoices 以添加其他属性?

How can I subclass Django TextChoices to add additional attributes?

我想在代码的其他地方使用 Django 3.0 TextChoices for a models.CharField choices optionEnum

这是我的代码的简化版本:

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

但 none 按预期给我直接字符串比较。

更新#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

你快到了。部分困难在于两个参数中的第一个,即元组 ('boolean', bool),正在传递给 Enum 机器。

所以,我们有两个选择:

  • 保留元组 as-is 并使用 index-access(您当前的工作解决方案):

    def __new__(cls, value): # value[0] is 'boolean'; value[1] is bool

  • 命名 __new__ 中的参数 header:

    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__ 的第一个参数是您要创建的实例的 class -- 通常与 __new__ 方法定义的相同 class . 尽管它看起来不像,__new__ 是一个 classmethod -- 它只是 special-cased 不需要 classmethod 装饰器。

这是我使用一些 metaclass 魔法所做的:

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看起来像

class ValueTypeModified(models.TextChoices, metaclass=Mwahaha):
     BOOLEAN = ('boolean', 'Boolean'), bool

cleardel 是绕过某些 Enum._dict 保护以防止覆盖枚举或属性所必需的。然而,这正是我们想要在这里做的。

可能有一种更简单的方法可以做到这一点而无需诉诸 metaclass 但我已经准备好并准备走那条路 ¯\_(ツ)_/¯

我发布了我自己的答案,这样我就可以解释我在@Ethan Furman 的帮助下学到了什么!

From your code it looks like value is ('boolean', bool), so when you do

obj = str.__new__(cls, value)

obj ends up being "('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__ 解释的那样,

The remaining arguments are those passed to the object constructor expression (the call to the class).

或者换句话说,我可以将剩余的参数视为直接输入 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

有效。