ตั้งค่า Django IntegerField ตามตัวเลือก = … name


109

เมื่อคุณมีฟิลด์โมเดลที่มีตัวเลือกตัวเลือกคุณมักจะมีค่าเวทย์มนตร์ที่เกี่ยวข้องกับชื่อที่มนุษย์อ่านได้ ใน Django มีวิธีที่สะดวกในการตั้งค่าฟิลด์เหล่านี้ด้วยชื่อที่มนุษย์อ่านได้แทนค่าหรือไม่?

พิจารณารุ่นนี้:

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

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

ในบางครั้งเรามีอินสแตนซ์ Thing และเราต้องการกำหนดลำดับความสำคัญ เห็นได้ชัดว่าคุณทำได้

thing.priority = 1

แต่นั่นบังคับให้คุณจำการแมป Value-Name ของ PRIORITIES สิ่งนี้ใช้ไม่ได้:

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

ขณะนี้ฉันมีวิธีแก้ปัญหาโง่ ๆ นี้:

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

แต่มันเป็นเรื่องที่น่าเบื่อ เนื่องจากสถานการณ์นี้เป็นเรื่องธรรมดาที่ฉันสงสัยว่าใครมีวิธีแก้ปัญหาที่ดีกว่านี้ มีวิธีฟิลด์บางวิธีในการตั้งค่าฟิลด์ตามชื่อตัวเลือกที่ฉันมองข้ามไปทั้งหมดหรือไม่?

คำตอบ:


164

ทำตามที่เห็นนี้ . จากนั้นคุณสามารถใช้คำที่แสดงจำนวนเต็มที่เหมาะสม

ชอบมาก:

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

จากนั้นพวกมันยังคงเป็นจำนวนเต็มในฐานข้อมูล

การใช้งานจะเป็น thing.priority = Thing.NORMAL


2
นั่นเป็นบล็อกที่มีรายละเอียดดีเกี่ยวกับเรื่องนี้ หายากด้วย Google ขอบคุณมาก
Alexander Ljungberg

1
FWIW หากคุณต้องการตั้งค่าจากสตริงตัวอักษร (อาจมาจากรูปแบบอินพุตของผู้ใช้หรือสิ่งที่คล้ายกัน) คุณสามารถทำได้: thing.priority = getattr (thing, strvalue.upper ())
mrooney

1
ชอบส่วน Encapsulation ในบล็อกมาก
Nathan Keller

ฉันมีปัญหา: ฉันมักจะเห็นค่าเริ่มต้นในผู้ดูแลระบบ! ฉันทดสอบแล้วว่าค่าเปลี่ยนไปจริงๆ! สิ่งที่ฉันควรทำตอนนี้?
Mahdi

นี่เป็นวิธีที่จะไป แต่ระวัง: หากคุณเพิ่มหรือลบตัวเลือกในอนาคตตัวเลขของคุณจะไม่เรียงตามลำดับ คุณสามารถแสดงความคิดเห็นเกี่ยวกับตัวเลือกที่เลิกใช้แล้วได้ดังนั้นจะไม่มีความสับสนในอนาคตและคุณจะไม่พบกับการชนกันของฐานข้อมูล
grokpot

7

ฉันอาจตั้งค่า reverse-lookup dict ครั้งแล้วครั้งเล่า แต่ถ้าฉันไม่ได้ฉันจะใช้:

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

ซึ่งดูเหมือนง่ายกว่าการสร้าง dict ในทันทีเพียงแค่โยนมันออกไปอีกครั้ง ;-)


ใช่การโยนคำสั่งเป็นเรื่องงี่เง่าเล็กน้อยตอนนี้ที่คุณพูด :)
Alexander Ljungberg

7

นี่คือประเภทฟิลด์ที่ฉันเขียนเมื่อไม่กี่นาทีก่อนซึ่งฉันคิดว่าทำในสิ่งที่คุณต้องการ ตัวสร้างต้องการอาร์กิวเมนต์ 'ตัวเลือก' ซึ่งอาจเป็นทูเปิลของ 2 ทูเปิลในรูปแบบเดียวกับอ็อพชันตัวเลือกสำหรับ IntegerField หรือแทนรายการชื่อธรรมดา (เช่น ChoiceField (('ต่ำ', 'ปกติ', 'สูง') ค่าเริ่มต้น = 'ต่ำ')) คลาสจะดูแลการแมปจากสตริงเป็น 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]

1
นั่นไม่เลวเลยอัลลัน ขอบคุณ!
Alexander Ljungberg

5

ฉันชื่นชมวิธีการกำหนดอย่างต่อเนื่อง แต่ฉันเชื่อว่าประเภทEnumนั้นดีที่สุดสำหรับงานนี้ สามารถแสดงจำนวนเต็มและสตริงสำหรับรายการในเวลาเดียวกันในขณะที่ทำให้โค้ดของคุณอ่านง่ายขึ้น

Enums ได้รับการแนะนำให้รู้จักกับ Python ในเวอร์ชัน 3.4 หากคุณกำลังใช้ใด ๆ ที่ต่ำกว่า (เช่น v2.x) คุณยังสามารถมีได้โดยการติดตั้งแพคเกจ backportedpip 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()

1
หากคุณไม่ต้องการแนะนำคลาสพื้นฐานเช่นChoiceEnumคุณสามารถใช้.valueและ.nameตามที่ @kirpit อธิบายและแทนที่การใช้งานchoices()ด้วย - tuple([(x.value, x.name) for x in cls])ทั้งในฟังก์ชัน (DRY) หรือโดยตรงในตัวสร้างของฟิลด์
Seth

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)

3

ใน 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

1

เพียงแค่แทนที่ตัวเลขของคุณด้วยค่าที่มนุษย์อ่านได้ที่คุณต้องการ ดังต่อไปนี้:

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

สิ่งนี้ทำให้มนุษย์สามารถอ่านได้อย่างไรก็ตามคุณต้องกำหนดการสั่งซื้อของคุณเอง


1

คำตอบของฉันช้ามากและอาจดูเหมือนชัดเจนสำหรับผู้เชี่ยวชาญของ Django ในปัจจุบัน แต่สำหรับใครก็ตามที่มาที่นี่ฉันเพิ่งค้นพบวิธีแก้ปัญหาที่หรูหรามากที่นำโดย django-model-utils: https://django-model-utils.readthedocs.io/ en / latest / utilities.html # options

แพคเกจนี้ช่วยให้คุณกำหนดตัวเลือกด้วยสาม tuples โดยที่:

  • รายการแรกคือค่าฐานข้อมูล
  • รายการที่สองคือค่าที่อ่านรหัสได้
  • รายการที่สามคือค่าที่มนุษย์อ่านได้

นี่คือสิ่งที่คุณสามารถทำได้:

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)

ทางนี้:

  • คุณสามารถใช้ค่าที่มนุษย์สามารถอ่านได้เพื่อเลือกค่าของฟิลด์ของคุณ (ในกรณีของฉันมันมีประโยชน์เพราะฉันกำลังคัดลอกเนื้อหาที่ไม่เหมาะสมและจัดเก็บด้วยวิธีที่เป็นมาตรฐาน)
  • ค่าที่สะอาดจะถูกเก็บไว้ในฐานข้อมูลของคุณ
  • คุณไม่มีอะไรที่ไม่แห้งให้ทำ;)

สนุก :)


1
ถูกต้องโดยสิ้นเชิงเพื่อเรียกใช้ django-model-utils ข้อเสนอแนะเล็กน้อยเพื่อเพิ่มความสามารถในการอ่าน ใช้สัญลักษณ์จุดบนพารามิเตอร์เริ่มต้นด้วยเช่นกัน: priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(ไม่จำเป็นต้องพูดว่าการกำหนดลำดับความสำคัญจะต้องถูกเยื้องเพื่อให้อยู่ในระดับ Thing เพื่อที่จะทำเช่นนั้น) ลองพิจารณาใช้คำ / อักขระตัวพิมพ์เล็กสำหรับตัวระบุ python เนื่องจากคุณไม่ได้อ้างถึงคลาส แต่ใช้พารามิเตอร์แทน (ตัวเลือกของคุณจะกลายเป็น: (0, 'low', 'Low'),และอื่น ๆ )
คิม

0

เดิมทีฉันใช้คำตอบของ @ 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)

0

ตัวเลือกตัวเลือกของโมเดลยอมรับลำดับที่ประกอบด้วยตัวมันเองของรายการซ้ำสองรายการ (เช่น [(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.HIGH, ThingPriority.['HIGH']หรือThingPriority(0)ในการเข้าถึงหรือสมาชิก enum ค้นหา

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.