Đặt Django IntegerField theo lựa chọn =… tên


109

Khi bạn có một trường mô hình với tùy chọn lựa chọn, bạn có xu hướng có một số giá trị ma thuật được liên kết với tên con người có thể đọc được. Có cách nào trong Django thuận tiện để đặt các trường này bằng tên có thể đọc được của con người thay vì giá trị không?

Hãy xem xét mô hình này:

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

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

Tại một thời điểm nào đó, chúng tôi có một cá thể Thing và chúng tôi muốn đặt mức độ ưu tiên của nó. Rõ ràng là bạn có thể làm,

thing.priority = 1

Nhưng điều đó buộc bạn phải ghi nhớ ánh xạ Giá trị-Tên các ƯU TIÊN. Điều này không hoạt động:

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

Hiện tại tôi có cách giải quyết ngớ ngẩn này:

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

nhưng điều đó thật khó hiểu. Với mức độ phổ biến của kịch bản này, tôi đã tự hỏi liệu có ai có giải pháp tốt hơn không. Có một số phương pháp trường để đặt trường theo tên lựa chọn mà tôi hoàn toàn bỏ qua không?

Câu trả lời:


164

Làm như đã thấy ở đây . Sau đó, bạn có thể sử dụng một từ đại diện cho số nguyên thích hợp.

Như vậy:

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

Khi đó chúng vẫn là số nguyên trong DB.

Cách sử dụng sẽ là thing.priority = Thing.NORMAL


2
Đó là một bài đăng trên blog chi tiết về chủ đề này. Khó tìm với Google quá, cảm ơn.
Alexander Ljungberg

1
FWIW, nếu bạn cần đặt nó từ một chuỗi ký tự (có thể từ biểu mẫu, thông tin người dùng nhập hoặc tương tự) thì bạn có thể thực hiện: thing.priasty = getattr (thing, strvalue.upper ()).
mrooney

1
Thực sự thích phần Encapsulation trên blog.
Nathan Keller

Tôi gặp sự cố: Tôi luôn thấy giá trị mặc định trên quản trị viên! Tôi đã kiểm tra rằng giá trị thực sự thay đổi! Tôi nên làm gì bây giờ?
Mahdi

Đây là cách để thực hiện nhưng hãy cẩn thận: nếu bạn thêm hoặc xóa các lựa chọn trong tương lai, các số của bạn sẽ không tuần tự. Bạn có thể nhận xét về các lựa chọn không được dùng nữa để không có sự nhầm lẫn trong tương lai và bạn sẽ không gặp phải va chạm db.
grokpot,

7

Tôi có thể đã thiết lập lệnh tra cứu ngược một lần và mãi mãi, nhưng nếu không, tôi chỉ cần sử dụng:

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

điều này có vẻ đơn giản hơn việc xây dựng lệnh ngay lập tức chỉ để ném nó đi một lần nữa ;-).


Vâng, tung ra câu lệnh là một chút ngớ ngẩn, bây giờ bạn nói nó. :)
Alexander Ljungberg

7

Đây là một loại trường mà tôi đã viết cách đây vài phút mà tôi nghĩ nó phù hợp với những gì bạn muốn. Hàm khởi tạo của nó yêu cầu một đối số 'lựa chọn', có thể là một bộ gồm 2 bộ có cùng định dạng với tùy chọn lựa chọn đối với IntegerField hoặc thay vào đó là một danh sách tên đơn giản (ví dụ: ChoiceField (('Thấp', 'Bình thường', 'Cao'), mặc định = 'Thấp')). Lớp sẽ chăm sóc ánh xạ từ chuỗi sang int cho bạn, bạn không bao giờ thấy 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
Đó không phải là xấu Allan. Cảm ơn!
Alexander Ljungberg 29/09/09

5

Tôi đánh giá cao cách xác định liên tục nhưng tôi tin rằng loại Enum là tốt nhất cho nhiệm vụ này. Chúng có thể đại diện cho số nguyên và một chuỗi cho một mục trong cùng một thời điểm, đồng thời giữ cho mã của bạn dễ đọc hơn.

Enums đã được giới thiệu với Python trong phiên bản 3.4. Nếu bạn đang sử dụng bất kỳ thấp hơn nào (chẳng hạn như v2.x), bạn vẫn có thể có nó bằng cách cài đặt gói backported :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++'

Đây là khá nhiều tất cả. Bạn có thể kế thừaChoiceEnum để tạo định nghĩa của riêng mình và sử dụng chúng trong định nghĩa mô hình như:

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

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

Truy vấn đóng băng trên bánh như bạn có thể đoán:

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

Biểu diễn chúng trong chuỗi cũng được thực hiện dễ dàng:

# 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
Nếu bạn không muốn giới thiệu một lớp cơ sở như thế nào ChoiceEnum, bạn có thể sử dụng .value.namenhư @kirpit mô tả và thay thế việc sử dụng choices()bằng tuple([(x.value, x.name) for x in cls])--e hoặc trong một hàm (DRY) hoặc trực tiếp trong hàm tạo của trường.
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)

Sử dụng nó như thế này:

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

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

3

Kể từ Django 3.0, bạn có thể sử dụng:

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

Chỉ cần thay thế các số của bạn bằng các giá trị mà con người có thể đọc được mà bạn muốn. Như vậy:

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

Điều này giúp con người có thể đọc được, tuy nhiên, bạn phải xác định thứ tự của riêng mình.


1

Câu trả lời của tôi là rất muộn và có vẻ hiển nhiên đối với các chuyên gia ngày nay-Django, nhưng đối với bất kỳ ai đặt chân đến đây, gần đây tôi đã phát hiện ra một giải pháp rất hữu ích do django-model-utils mang lại: https://django-model-utils.readthedocs.io/ vi / mới nhất / tiện ích.html # lựa chọn

Gói này cho phép bạn xác định Lựa chọn với ba bộ giá trị trong đó:

  • Mục đầu tiên là giá trị cơ sở dữ liệu
  • Mục thứ hai là một giá trị có thể đọc được mã
  • Mục thứ ba là giá trị con người có thể đọc được

Vì vậy, đây là những gì bạn có thể làm:

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)

Cách này:

  • Bạn có thể sử dụng giá trị mà con người có thể đọc được để thực sự chọn giá trị cho trường của bạn (trong trường hợp của tôi, nó hữu ích vì tôi đang cắt nội dung hoang dã và lưu trữ nó theo cách chuẩn hóa)
  • Giá trị sạch được lưu trữ trong cơ sở dữ liệu của bạn
  • Bạn không có gì không KHÔ để làm;)

Thưởng thức :)


1
Hoàn toàn hợp lệ để đưa ra django-model-utils. Một gợi ý nhỏ để tăng khả năng đọc; cũng sử dụng ký hiệu dấu chấm trên tham số mặc định: priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(không cần phải nói, việc gán mức độ ưu tiên phải được thụt vào bên trong lớp Thing để làm như vậy). Ngoài ra, hãy cân nhắc sử dụng các từ / ký tự viết thường cho mã định danh python, vì bạn không tham chiếu đến một lớp mà thay vào đó là một tham số (Lựa chọn của bạn sau đó sẽ trở thành: (0, 'low', 'Low'),và v.v.).
Kim

0

Ban đầu, tôi đã sử dụng phiên bản sửa đổi của câu trả lời của @ 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))

Đơn giản hóa với django-enumfieldsgói gói mà tôi hiện đang sử dụng:

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

Tùy chọn lựa chọn của mô hình chấp nhận một trình tự bao gồm các tệp lặp của chính xác hai mục (ví dụ: [(A, B), (A, B) ...]) để sử dụng làm lựa chọn cho trường này.

Ngoài ra, Django cung cấp các kiểu liệt kê mà bạn có thể phân lớp để xác định các lựa chọn một cách ngắn gọn:

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 hỗ trợ thêm một giá trị chuỗi bổ sung vào cuối bộ tuple này để được sử dụng làm tên hoặc nhãn mà con người có thể đọc được. Nhãn có thể là một chuỗi có thể dịch lười biếng.

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

Lưu ý: bạn có thể sử dụng mà sử dụng ThingPriority.HIGH, ThingPriority.['HIGH']hoặc ThingPriority(0)truy cập hoặc các thành viên enum tra cứu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.