问题:通过choices =…设置Django IntegerField

当您拥有一个带有选项选项的模型字段时,您倾向于具有一些与人类可读名称相关的魔术值。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']

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

When you have a model field with a choices option you tend to have some magic values associated with human readable names. Is there in Django a convenient way to set these fields by the human readable name instead of the value?

Consider this model:

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

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

At some point we have a Thing instance and we want to set its priority. Obviously you could do,

thing.priority = 1

But that forces you to memorize the Value-Name mapping of PRIORITIES. This doesn’t work:

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

Currently I have this silly workaround:

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

but that’s clunky. Given how common this scenario could be I was wondering if anyone had a better solution. Is there some field method for setting fields by choice name which I totally overlooked?


回答 0

如做在这里看到。然后,您可以使用代表适当整数的单词。

像这样:

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

然后它们仍然是数据库中的整数。

用法是 thing.priority = Thing.NORMAL

Do as seen here. Then you can use a word that represents the proper integer.

Like so:

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

Then they are still integers in the DB.

Usage would be thing.priority = Thing.NORMAL


回答 1

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

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

这似乎比立即构建dict再扔掉要简单得多;-)。

I’d probably set up the reverse-lookup dict once and for all, but if I hadn’t I’d just use:

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

which seems simpler than building the dict on the fly just to toss it away again;-).


回答 2

这是我几分钟前写的一种字段类型,我认为它满足您的要求。它的构造函数需要一个参数“ choices”,它可以是2元组的元组,格式与IntegerField的choices选项相同,也可以是简单的名称列表(例如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]

Here’s a field type I wrote a few minutes ago that I think does what you want. Its constructor requires an argument ‘choices’, which may be either a tuple of 2-tuples in the same format as the choices option to IntegerField, or instead a simple list of names (ie ChoiceField((‘Low’, ‘Normal’, ‘High’), default=’Low’) ). The class takes care of the mapping from string to int for you, you never see the 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]

回答 3

我欣赏不断的定义方式,但我相信枚举类型最适合此任务。它们可以同时表示一个项目的整数和字符串,同时保持代码的可读性。

枚举在版本3.4中引入Python。如果您使用的是任何较低版本(例如v2.x),则仍可以通过安装向后移植的软件包来安装它: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()

I appreciate the constant defining way but I believe Enum type is far best for this task. They can represent integer and a string for an item in the same time, while keeping your code more readable.

Enums were introduced to Python in version 3.4. If you are using any lower (such as v2.x) you can still have it by installing the 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++'

This is pretty much all. You can inherit the ChoiceEnum to create your own definitions and use them in a model definition like:

from django.db import models
from myapp.fields import Language

class MyModel(models.Model):
    language = models.IntegerField(choices=Language.choices(), default=int(Language.Python))
    # ...

Querying is icing on the cake as you may guess:

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

Representing them in string is also made easy:

# 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()

回答 4

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)
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)

Use it like this:

Priorities = Enum(
    ('LOW', 'Low'),
    ('NORMAL', 'Normal'),
    ('HIGH', 'High')
)

priority = models.IntegerField(default=Priorities.LOW, choices=Priorities)

回答 5

从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

As of Django 3.0, you can use:

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

回答 6

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

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

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

Simply replace your numbers with the human readable values you would like. As such:

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

This makes it human readable, however, you’d have to define your own ordering.


回答 7

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

此程序包使您可以定义具有三元组的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可以做的事;)

请享用 :)

My answer is very late and might seem obvious to nowadays-Django experts, but to whoever lands here, i recently discovered a very elegant solution brought by django-model-utils: https://django-model-utils.readthedocs.io/en/latest/utilities.html#choices

This package allows you to define Choices with three-tuples where:

  • The first item is the database value
  • The second item is a code-readable value
  • The third item is a human-readable value

So here’s what you can do:

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)

This way:

  • You can use your human-readable value to actually choose the value of your field (in my case, it’s useful because i’m scraping wild content and storing it in a normalized way)
  • A clean value is stored in your database
  • You have nothing non-DRY to do ;)

Enjoy :)


回答 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)

Originally I used a modified version of @Allan’s answer:

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))

Simplified with the django-enumfields package package which I now use:

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

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

另外,Django提供 枚举类型,您可以枚举为子类,以简洁的方式定义选择:

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)访问或查询枚举成员。

Model’s choices option accepts a sequence consisting itself of iterables of exactly two items (e.g. [(A, B), (A, B) …]) to use as choices for this field.

In addition, Django provides enumeration types that you can subclass to define choices in a concise way:

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 supports adding an extra string value to the end of this tuple to be used as the human-readable name, or label. The label can be a lazy translatable string.

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

Note: you can use that using ThingPriority.HIGH, ThingPriority.['HIGH'], or ThingPriority(0) to access or lookup enum members.


声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。