【问题标题】:Set Django IntegerField by choices=... name通过选择设置 Django IntegerField=...名称
【发布时间】:2010-11-10 04:39:44
【问题描述】:

当您有一个带有选择选项的模型字段时,您往往会有一些与人类可读名称相关联的神奇值。在 Django 中是否有一种方便的方法可以通过人类可读的名称而不是值来设置这些字段?

考虑这个模型:

class Thing(models.Model):
  PRIORITIES = (
    (0, 'Low'),
    (1, 'Normal'),
    (2, 'High'),
  )

  priority = models.IntegerField(default=0, choices=PRIORITIES)

在某些时候,我们有一个 Thing 实例,我们想要设置它的优先级。显然你可以做到,

thing.priority = 1

但这迫使您记住优先级的值-名称映射。这不起作用:

thing.priority = 'Normal' # Throws ValueError on .save()

目前我有这个愚蠢的解决方法:

thing.priority = dict((key,value) for (value,key) in Thing.PRIORITIES)['Normal']

但这很笨拙。考虑到这种情况有多普遍,我想知道是否有人有更好的解决方案。是否有一些我完全忽略的按选择名称设置字段的字段方法?

【问题讨论】:

    标签: python django django-models


    【解决方案1】:

    只需将数字替换为您想要的人类可读值即可。因此:

    PRIORITIES = (
    ('LOW', 'Low'),
    ('NORMAL', 'Normal'),
    ('HIGH', 'High'),
    )
    

    这使它易于阅读,但是,您必须定义自己的顺序。

    【讨论】:

      【解决方案2】:

      我可能会一劳永逸地设置反向查找字典,但如果我没有,我会使用:

      thing.priority = next(value for value, name in Thing.PRIORITIES
                            if name=='Normal')
      

      这似乎比在运行中构建 dict 只是为了再次扔掉它更简单;-)。

      【讨论】:

      • 是的,折腾字典有点傻,既然你这么说了。 :)
      【解决方案3】:

      按照seen here 做。然后你可以用一个词来表示正确的整数。

      像这样:

      LOW = 0
      NORMAL = 1
      HIGH = 2
      STATUS_CHOICES = (
          (LOW, 'Low'),
          (NORMAL, 'Normal'),
          (HIGH, 'High'),
      )
      

      那么它们在 DB 中仍然是整数。

      用法为thing.priority = Thing.NORMAL

      【讨论】:

      • 这是一篇关于该主题的非常详细的博客文章。用 Google 也很难找到,所以谢谢。
      • FWIW,如果您需要从文字字符串(可能来自表单、用户输入或类似内容)设置它,您可以这样做:thing.priority = getattr(thing, strvalue.upper() )。
      • 真的很喜欢博客上的封装部分。
      • 我有一个问题:我总是在管理员上看到默认值!我已经测试过该值确实发生了变化!我现在该怎么办?
      • 这是要走的路,但要注意:如果您以后添加或删除选项,您的数字将不连续。您可以注释掉不推荐使用的选项,这样就不会在未来产生混乱,也不会遇到数据库冲突。
      【解决方案4】:

      这是我几分钟前写的一个字段类型,我认为它可以满足您的需求。它的构造函数需要一个参数“choices”,它可以是与 IntegerField 的选择选项格式相同的 2 元组的元组,或者是一个简单的名称列表(即 ChoiceField(('Low', 'Normal', '高'),默认='低'))。该类为您处理从字符串到 int 的映射,您永远看不到 int。

        class ChoiceField(models.IntegerField):
          def __init__(self, choices, **kwargs):
              if not hasattr(choices[0],'__iter__'):
                  choices = zip(range(len(choices)), choices)
      
              self.val2choice = dict(choices)
              self.choice2val = dict((v,k) for k,v in choices)
      
              kwargs['choices'] = choices
              super(models.IntegerField, self).__init__(**kwargs)
      
          def to_python(self, value):
              return self.val2choice[value]
      
          def get_db_prep_value(self, choice):
              return self.choice2val[choice]
      

      【讨论】:

      • 艾伦还不错。谢谢!
      【解决方案5】:
      class Sequence(object):
          def __init__(self, func, *opts):
              keys = func(len(opts))
              self.attrs = dict(zip([t[0] for t in opts], keys))
              self.choices = zip(keys, [t[1] for t in opts])
              self.labels = dict(self.choices)
          def __getattr__(self, a):
              return self.attrs[a]
          def __getitem__(self, k):
              return self.labels[k]
          def __len__(self):
              return len(self.choices)
          def __iter__(self):
              return iter(self.choices)
          def __deepcopy__(self, memo):
              return self
      
      class Enum(Sequence):
          def __init__(self, *opts):
              return super(Enum, self).__init__(range, *opts)
      
      class Flags(Sequence):
          def __init__(self, *opts):
              return super(Flags, self).__init__(lambda l: [1<<i for i in xrange(l)], *opts)
      

      像这样使用它:

      Priorities = Enum(
          ('LOW', 'Low'),
          ('NORMAL', 'Normal'),
          ('HIGH', 'High')
      )
      
      priority = models.IntegerField(default=Priorities.LOW, choices=Priorities)
      

      【讨论】:

        【解决方案6】:

        我很欣赏不断定义的方式,但我相信Enum 类型最适合这项任务。它们可以同时表示一个项目的整数和字符串,同时使您的代码更具可读性。

        枚举是在 3.4 版中引入 Python 的。如果您使用的是任何较低版本(例如 v2.x),您仍然可以通过安装 backported package:pip install enum34 来拥有它。

        # myapp/fields.py
        from enum import Enum    
        
        
        class ChoiceEnum(Enum):
        
            @classmethod
            def choices(cls):
                choices = list()
        
                # Loop thru defined enums
                for item in cls:
                    choices.append((item.value, item.name))
        
                # return as tuple
                return tuple(choices)
        
            def __str__(self):
                return self.name
        
            def __int__(self):
                return self.value
        
        
        class Language(ChoiceEnum):
            Python = 1
            Ruby = 2
            Java = 3
            PHP = 4
            Cpp = 5
        
        # Uh oh
        Language.Cpp._name_ = 'C++'
        

        差不多就这些了。您可以继承 ChoiceEnum 来创建自己的定义并在模型定义中使用它们,例如:

        from django.db import models
        from myapp.fields import Language
        
        class MyModel(models.Model):
            language = models.IntegerField(choices=Language.choices(), default=int(Language.Python))
            # ...
        

        您可能猜到,查询是锦上添花:

        MyModel.objects.filter(language=int(Language.Ruby))
        # or if you don't prefer `__int__` method..
        MyModel.objects.filter(language=Language.Ruby.value)
        

        用字符串表示它们也很容易:

        # Get the enum item
        lang = Language(some_instance.language)
        
        print(str(lang))
        # or if you don't prefer `__str__` method..
        print(lang.name)
        
        # Same as get_FOO_display
        lang.name == some_instance.get_language_display()
        

        【讨论】:

        • 如果您不想引入像ChoiceEnum 这样的基类,您可以使用@kirpit 描述的.value.name,并将choices() 的用法替换为tuple([(x.value, x.name) for x in cls])- - 在函数中 (DRY) 或直接在字段的构造函数中。
        【解决方案7】:

        我的回答已经很晚了,对于现在的 Django 专家来说似乎很明显,但是对于来到这里的人来说,我最近发现了 django-model-utils 带来的一个非常优雅的解决方案:https://django-model-utils.readthedocs.io/en/latest/utilities.html#choices

        此包允许您使用三元组定义选项,其中:

        • 第一项是数据库值
        • 第二项是代码可读值
        • 第三项是人类可读的值

        所以你可以这样做:

        from model_utils import Choices
        
        class Thing(models.Model):
            PRIORITIES = Choices(
                (0, 'low', 'Low'),
                (1, 'normal', 'Normal'),
                (2, 'high', 'High'),
              )
        
            priority = models.IntegerField(default=PRIORITIES.normal, choices=PRIORITIES)
        
        thing.priority = getattr(Thing.PRIORITIES.Normal)
        

        这边:

        • 您可以使用人类可读的值来实际选择字段的值(在我的情况下,这很有用,因为我正在抓取野生内容并以标准化方式存储它)
        • 一个干净的值存储在您的数据库中
        • 您没有非 DRY 可做 ;)

        享受:)

        【讨论】:

        • 完全可以启动 django-model-utils。增加可读性的一个小建议;在默认参数上也使用点符号:priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(不用说,优先级分配必须缩进到 Thing 类中才能这样做)。还可以考虑为 python 标识符使用小写单词/字符,因为您不是指一个类,而是一个参数(您的选择将变为:(0, 'low', 'Low'), 等等)。
        【解决方案8】:

        最初我使用了@Allan 答案的修改版本:

        from enum import IntEnum, EnumMeta
        
        class IntegerChoiceField(models.IntegerField):
            def __init__(self, choices, **kwargs):
                if hasattr(choices, '__iter__') and isinstance(choices, EnumMeta):
                    choices = list(zip(range(1, len(choices) + 1), [member.name for member in list(choices)]))
        
                kwargs['choices'] = choices
                super(models.IntegerField, self).__init__(**kwargs)
        
            def to_python(self, value):
                return self.choices(value)
        
            def get_db_prep_value(self, choice):
                return self.choices[choice]
        
        models.IntegerChoiceField = IntegerChoiceField
        
        GEAR = IntEnum('GEAR', 'HEAD BODY FEET HANDS SHIELD NECK UNKNOWN')
        
        class Gear(Item, models.Model):
            # Safe to assume last element is largest value member of an enum?
            #type = models.IntegerChoiceField(GEAR, default=list(GEAR)[-1].name)
            largest_member = GEAR(max([member.value for member in list(GEAR)]))
            type = models.IntegerChoiceField(GEAR, default=largest_member)
        
            def __init__(self, *args, **kwargs):
                super(Gear, self).__init__(*args, **kwargs)
        
                for member in GEAR:
                    setattr(self, member.name, member.value)
        
        print(Gear().HEAD, (Gear().HEAD == GEAR.HEAD.value))
        

        使用我现在使用的 django-enumfields 包进行了简化:

        from enumfields import EnumIntegerField, IntEnum
        
        GEAR = IntEnum('GEAR', 'HEAD BODY FEET HANDS SHIELD NECK UNKNOWN')
        
        class Gear(Item, models.Model):
            # Safe to assume last element is largest value member of an enum?
            type = EnumIntegerField(GEAR, default=list(GEAR)[-1])
            #largest_member = GEAR(max([member.value for member in list(GEAR)]))
            #type = EnumIntegerField(GEAR, default=largest_member)
        
            def __init__(self, *args, **kwargs):
                super(Gear, self).__init__(*args, **kwargs)
        
                for member in GEAR:
                    setattr(self, member.name, member.value)
        

        【讨论】:

          【解决方案9】:

          从 Django 3.0 开始,您可以使用:

          class ThingPriority(models.IntegerChoices):
              LOW = 0, 'Low'
              NORMAL = 1, 'Normal'
              HIGH = 2, 'High'
          
          
          class Thing(models.Model):
              priority = models.IntegerField(default=ThingPriority.LOW, choices=ThingPriority.choices)
          
          # then in your code
          thing = get_my_thing()
          thing.priority = ThingPriority.HIGH
          

          【讨论】:

            【解决方案10】:

            模型的选择选项接受一个序列,该序列由恰好两个项目的可迭代项组成(例如 [(A, B), (A, B) ...]),用作该字段的选择。

            此外,Django 提供了enumeration types,您可以对其进行子类化以简洁地定义选择:

            from django.utils.translation import gettext_lazy as _
            
            class ThingPriority(models.IntegerChoices):
                LOW = 0, _('Low')
                NORMAL = 1, _('Normal')
                HIGH = 2, _('High')
            
            class Thing(models.Model):
                priority = models.IntegerField(default=ThingPriority.NORMAL, choices=ThingPriority.choices)
            
            

            Django 支持在这个元组的末尾添加一个额外的字符串值,用作人类可读的名称或标签。标签可以是惰性可翻译字符串。

               # in your code 
               thing = get_thing() # instance of Thing
               thing.priority = ThingPriority.LOW
            

            注意:您可以使用 ThingPriority.HIGHThingPriority.['HIGH']ThingPriority(0) 来访问或查找枚举成员。

            你需要导入from django.utils.translation import gettext_lazy as _

            【讨论】:

            • 我认为这个答案不起作用,但我忘记了“从 django.utils.translation import gettext_lazy as _”
            猜你喜欢
            • 1970-01-01
            • 2016-08-10
            • 1970-01-01
            • 2014-05-18
            • 1970-01-01
            • 2014-06-11
            • 2011-03-29
            • 2013-10-28
            相关资源
            最近更新 更多