在Django官方文档中,它们提供了两个关于如何使用预定义选项作为特定字段选择的示例
官方文档的例子
第一个例子中定义choices是用一个元组包裹的元组,里面元组的第一个元素的值是设置在model的值, 第二个元素的值是它的字符串表示。
YEAR_IN_SCHOOL_CHOICES = [
('FR', 'Freshman'),
('SO', 'Sophomore'),
('JR', 'Junior'),
('SR', 'Senior'),
]
另一个例子, 官方建议我们定义选项为model类的常量。
from django.db import models
class Student(models.Model):
FRESHMAN = 'FR'
SOPHOMORE = 'SO'
JUNIOR = 'JR'
SENIOR = 'SR'
YEAR_IN_SCHOOL_CHOICES = [
(FRESHMAN, 'Freshman'),
(SOPHOMORE, 'Sophomore'),
(JUNIOR, 'Junior'),
(SENIOR, 'Senior'),
]
year_in_school = models.CharField(
max_length=2,
choices=YEAR_IN_SCHOOL_CHOICES,
default=FRESHMAN,
)
def is_upperclass(self):
return self.year_in_school in (self.JUNIOR, self.SENIOR)
可以很明显的看出, 第二种方式是开发友好的。
使用枚举
什么是枚举
我们先来看官方定义:
An enumeration is a set of symbolic names (members) bound to unique, constant values. Within an enumeration, the members can be compared by identity, and the enumeration itself can be iterated over.
翻译一下就是:
枚举是一组绑定到唯一常量值的符号名称(成员)。在枚举中,可以通过标识来比较成员,并且可以迭代枚举本身。
定义枚举
下面举一个简单的例子——假设有一个字段来识别一本书的语言,语言的选择是英语和中文。
首先,为这些所有的选项定义一个类:
from enum import Enum
class LanguageChoice(Enum):
"""语言枚举"""
English = "EN"
Chinese = "CN"
然后在model里做如下定义:
from django.db import models
class Book(models.Model):
language = models.CharField(
max_length=5,
choices=[(tag.value, tag.name) for tag in LanguageChoice]
)
CharField
字段的choices
接收一个可迭代对象,在这个Book模型下,语言的选项是一个由元组构成的列表。为了简单起见,我们用了列表生成式生成我们的列表。
对于选择中的每个元组,第一个元素是存储到模型中的值,而第二个元素是其人类可读的表示。
使用枚举
现在我们已经定义了我们的枚举和模型,我们将如何使用它?假设我们要添加一个语言为'CH'的Book条目:
b = Book(language=LanguageChoice.Chinese.value)
b.save()
为什么要用枚举
可能有人会问:“为什么使用Enum而不是Django官方文档中给出的示例?”
预定义值在内存中,Enumeration可用于整个程序,而不仅仅是模型中。这样相对来说对人类更加友好,并且如果某一相对应的值有变化时,并不需要更改代码。
什么时候使用Enum?
根据PEP435,枚举对于定义可能具有或不具有语义含义的不可变的,相关的常量值集合是有用的。
例如:错误状态值和定义过程中的状态。
更优雅的代码
上述的使用方法看起来还是有点奇怪,在我们定义model
的时候不得不手动写一个循环。事实上只需要重载枚举类的__iter__
即可满足django对choice
的可迭代要求。
在获取值时使用Languages.Chinese.value
,多一个.value
要多敲七下键盘,太麻烦。可以通过重载__getattribute__
方法来避免这种麻烦。
from enum import Enum, EnumMeta
class ChoiceEnumMeta(EnumMeta):
def __getattribute__(cls, name):
attr = super().__getattribute__(name)
if isinstance(attr, Enum):
return attr.value
return attr
def __iter__(self):
return ((tag.value, tag.name) for tag in super().__iter__())
class ChoiceEnum(Enum, metaclass=ChoiceEnumMeta):
"""
Enum for Django ChoiceField use.
"""
pass
样例
class MyModel(models.Model):
class Languages(ChoiceEnum):
Chinese = "ch"
English = "en"
French = "fr"
language = models.CharField(max_length=20, choices=Languages)
它等价于
class MyModel(models.Model):
Languages = (
("ch", "Chinese")
("en", "English")
("fr", "French")
)
language = models.CharField(max_length=20, choices=Languages)
更好的 Admin 支持
作为中国人,当然希望页面全部都是中文的。不过对于 Python 代码来说,中文变量虽然可行,但编辑器支持较差。那么下面将展示一种方法用于定义适合非英语母语人士使用的 Choice,它允许手动定义 Choice 的模板显示。
延续之前的代码,但对__iter__
做一些修改,再定义一个 dict
方法以支持对 Choice 原始数据的遍历。
from enum import Enum, EnumMeta
class ChoiceEnumMeta(EnumMeta):
...
def __iter__(cls):
if hasattr(cls, "__admin__"):
admin = cls.__admin__()
return (
(tag.value, admin.get(tag.name, tag.name)) for tag in super().__iter__()
)
return ((tag.value, tag.name) for tag in super().__iter__())
def dict(cls):
return {tag.name: tag.value for tag in super().__iter__()}
下面是一个定义的例子:
class Status(ChoiceEnum):
reviewing = "0"
running = "1"
closed = "2"
locked = "3"
@staticmethod
def __admin__() -> typing.Dict[str, str]:
return {
"reviewing": "审核中",
"closed": "已关闭",
"running": "已上线",
"locked": "已锁定",
}
在代码中可以照常使用Status.reviewing
等英文名的属性,而在 Django 模板(譬如后台)中它将显示中文。