当您有一个带有选择选项的模型字段时,您往往会有一些与人类可读名称相关的神奇值。在 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']
但这很笨拙。考虑到这种情况有多普遍,我想知道是否有人有更好的解决方案。是否有一些我完全忽略的按选择名称设置字段的字段方法?
像 seen here 那样做。然后,您可以使用表示正确整数的单词。
像这样:
LOW = 0
NORMAL = 1
HIGH = 2
STATUS_CHOICES = (
(LOW, 'Low'),
(NORMAL, 'Normal'),
(HIGH, 'High'),
)
然后它们仍然是数据库中的整数。
用法为 thing.priority = Thing.NORMAL
从 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
我可能会一劳永逸地设置反向查找字典,但如果我没有,我只会使用:
thing.priority = next(value for value, name in Thing.PRIORITIES
if name=='Normal')
这似乎比在运行中构建 dict 只是为了再次将其扔掉更简单;-)。
这是我几分钟前写的一个字段类型,我认为它可以满足您的需求。它的构造函数需要一个参数“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]
模型的选择选项接受一个序列,该序列由恰好两个项目的可迭代项组成(例如 [(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.HIGH
、ThingPriority.['HIGH']
或 ThingPriority(0)
来访问或查找枚举成员。
您需要导入 from django.utils.translation import gettext_lazy as _
我很欣赏不断定义的方式,但我相信 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)或直接在字段的构造函数中。
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)
只需用您想要的人类可读值替换您的数字。像这样:
PRIORITIES = (
('LOW', 'Low'),
('NORMAL', 'Normal'),
('HIGH', 'High'),
)
这使它具有人类可读性,但是,您必须定义自己的顺序。
我的回答已经很晚了,对于现在的 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)
这边走:
您可以使用人类可读的值来实际选择字段的值(在我的情况下,它很有用,因为我正在抓取野生内容并以标准化方式存储它)
一个干净的值存储在您的数据库中
你无事可做;)
享受 :)
priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)
(不用说,优先级分配必须缩进到 Thing 类中才能这样做)。还可以考虑对 python 标识符使用小写单词/字符,因为您不是指一个类,而是一个参数(您的选择将变为:(0, 'low', 'Low'),
等等)。
最初我使用了@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)
不定期副业成功案例分享