【问题标题】:How can I subclass Django TextChoices to add additional attributes?如何子类化 Django TextChoices 以添加其他属性?
【发布时间】: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

【问题讨论】:

    标签: python django django-models enums metaclass


    【解决方案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
      

      有效。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-07-29
        • 1970-01-01
        • 1970-01-01
        • 2023-04-06
        • 2011-07-04
        • 1970-01-01
        • 2021-09-24
        • 1970-01-01
        相关资源
        最近更新 更多