Django, как вызвать метод из настраиваемого поля с учетом экземпляра модели? - PullRequest
1 голос
/ 26 сентября 2019

У меня есть следующая модель:

class CustomField(models.CharField):
    def foo(self):
        return 'foo'

class Test(models.Model):
    col1 = models.CharField(max_length=45)
    col2 = CustomField(max_length=45)

Как я могу вызвать метод foo из CustomField, если мне дан экземпляр Test?

Например:

>>> t = Test.objects.create(col1='bar', col2='blah')
>>> t.col2
'blah'
>>> t.col2.foo() # 'str' object has not attribute 'foo'
'foo'

Это, конечно, выдает:

Объект 'str' не имеет атрибута 'foo'

, потому что вызывает model_instance.column возвращает значение этого столбца, а не экземпляр column.

Но почему именно?Кажется, ORM Джанго волшебным образом преобразует экземпляр класса поля в значение.Я часами копался в исходном коде и, похоже, не могу найти место, где происходит преобразование.

TLDR;

Возможно ли вернуть экземпляр класса поля с учетом моделиэкземпляр?

Есть идеи, где это происходит в исходном коде Django?Я предполагаю, что это происходит в django/db/models/base.py, но этот файл содержит более 1800 строк кода, поэтому очень трудно сказать.

Обновление

Вот практический пример того, почему это будетполезно:

class TempField(models.DecimalField):
    def __init__(self, initial_unit='C', **kwargs):
        self.initial_unit = initial_unit
        self.units = ['F', 'C', 'K']

    def convert(self, unit):
        if self.initial_unit == unit:
            return self.value

        if unit not in self.units:
            raise

        attr = getattr(self, f'_{initial_unit}_to_{unit}', None)
        if attr is None:
            raise

        return attr(unit)

        def _C_to_F(self, unit):
            ...

Теперь вы можете удобно преобразовать это поле в желаемую единицу измерения:

class Test(models.Model):
    temperature = TempField(...)

>>>t = Test.objects.create(temperature=100)
>>>t.temperature
100
>>>t.temperature.convert('F')
212

Это всего лишь непроверенный псевдокод.Кроме того, я могу придумать несколько способов использования этой функциональности без головной боли при использовании настраиваемых полей таким образом;так что этот вопрос действительно о понимании того, как работает ORM в Django, а не о том, как решать какие-либо реальные проблемы.

Ответы [ 2 ]

1 голос
/ 27 сентября 2019

Вот слегка измененная, рабочая версия WillemVanOnsem's замечательный ответ :

class TemperatureField(models.DecimalField):

    def from_db_value(self, value, expression, connection):
        if value is not None:
            return Temperature(value)
        return None

    def to_python(self, value):
        if isinstance(value, Temperature):
            return value
        if value is None:
            return value
        kelvin = super().to_python(value)
        return Temperature(kelvin)

    def get_prep_value(self, value):
        if isinstance(value, Temperature):
            value = value.kelvin
        return super().get_prep_value(value)

    def get_db_prep_save(self, value, connection):
        if isinstance(value, Temperature):
            return connection.ops.adapt_decimalfield_value(value.kelvin, self.max_digits, self.decimal_places)
        elif isinstance(value, (float, int)):
            return connection.ops.adapt_decimalfield_value(Decimal(value), self.max_digits, self.decimal_places)
        elif isinstance(value, (Decimal,)):
            return connection.ops.adapt_decimalfield_value(Decimal(value), self.max_digits, self.decimal_places)

Test(models.Model):
    temp = TemperatureField(max_digits=10, decimal_places=2)

Несколько замечаний:

Для сохранения пользовательских типов полей в вашемDB, вы должны переопределить get_db_prep_value, чтобы ваша модель знала, как обрабатывать Temperature объекты, в противном случае ваша модель будет думать, что она работает с Decimal, что приведет к:

AttributeError: у объекта 'Temperature' нет атрибута 'quantize'

Устранить ошибку с легким исправлением ...

Теперь документы на from_db_value:

Если присутствует для подкласса поля, from_db_value () будет вызываться при любых обстоятельствах, когда данные загружаются из базы данных, включая вызовы агрегатов и values ​​().

акцент на при загрузке данных из базы данных !

Это означает, что при вызове t = Test.objects.create(...), from_db_value не будет оцениваться, и соответствующий пользовательский столбец для экземпляра t будет равен любому значению, которое вы установили в createоператор!

Например:

>>>t = Test.objects.create(temp=1)
>>>t.temp
1
>>>type(t.temp)
<class 'int'>

>>>t = Test.objects.first()
>>>t.temp
<extra_fields.fields.Temperature object at 0x10e733e50>
>>> type(t.temp)
<class 'extra_fields.fields.Temperature'>

Если вы попытались запустить исходную версию from_db_value:

def from_db_value(self, value):
    kelvin = super().from_db_value(value)
    if kelvin is not None:
        return Temperature(kelvin)
    return None

Вы даже не получите ошибок, покаВы вызываете:

>>>t = Test.objects.get(...)
TypeError: from_db_value() takes 2 positional arguments but 4 were given
AttributeError: 'super' object has no attribute 'from_db_value'

Наконец, заметьте , что from_db_value не является методом ни в одном из полей базовой модели Django, поэтому вызов super().from_db_value всегда вызовет ошибку.Вместо этого базовый класс Field проверит наличие from_db_value:

def get_db_converters(self, connection):
    if hasattr(self, 'from_db_value'):
        return [self.from_db_value]
    return []
1 голос
/ 26 сентября 2019

В информатике есть высказывание Дэвида Уилера , что " Все проблемы в информатике могут быть решены с помощью другого уровня косвенности (кроме слишком большого числа уровней косвенности) ".

Таким образом, мы можем определить класс Temperature, например, для хранения температуры:

from enum import Enum
from decimal import Decimal
NINE_FIFTHS = Decimal(9)/Decimal(5)

class TemperatureUnit(Enum):
    KELVIN = (1,0, 'K')
    FAHRENHEIT = (NINE_FIFTHS, Decimal('-459.67'), '°F')
    CELSIUS = (1, Decimal('-273.15'), '°C')
    RANKINE = (NINE_FIFTHS, 0, '°R')

class Temperature:

    def __init__(self, kelvin, unit=TemperatureUnit.CELSIUS):
        self.kelvin = Decimal(kelvin)
        self.unit = unit

    @staticmethod
    def from_unit(value, unit=TemperatureUnit.CELSIUS):
        a, b, *__ = unit.value
        return Temperature((value-b)/a, unit)

    @property
    def value(self):
        a, b, *__ = self.unit.value
        return a * self.kelvin + b

    def convert(self, unit):
        return Temperature(self.kelvin, unit)

    def __str__(self):
        return '{} {}'.format(self.value, self.unit.value[2])

Например, мы можем здесь создать температуры:

>>> str(Temperature(15, unit=TemperatureUnit.FAHRENHEIT))
'-432.67 °F'
>>> str(Temperature(0, unit=TemperatureUnit.FAHRENHEIT))
'-459.67 °F'
>>> str(Temperature(1, unit=TemperatureUnit.FAHRENHEIT))
'-457.87 °F'
>>> str(Temperature(0, unit=TemperatureUnit.FAHRENHEIT))
'-459.67 °F'
>>> str(Temperature(0, unit=TemperatureUnit.CELSIUS))
'-273.15 °C'

Теперь мыМожно создать поле модели Django, которое хранит и извлекает Temperature s, сохраняя их, например, в десятичном формате на стороне базы данных, в Кельвинах:

class TemperatureField(models.DecimalField):

    def from_db_value(self, value):
        kelvin = super().from_db_value(value)
        if kelvin is not None:
            return Temperature(kelvin)
        return None

    def to_python(self, value):
        if isinstance(value, Temperature):
            return value
        if value is None:
            return value
        kelvin = super().to_python(value)
        return Temperature(kelvin)

    def get_prep_value(self, value):
        if isinstance(value, Temperature):
            value = value.kelvin
        return super().get_prep_value(value)

Вышеприведенное, конечно, является простым наброском.См. документацию по написанию пользовательских полей модели для получения дополнительной информации.Вы можете добавить поле формы, виджет, поиск для запроса к базе данных и т. Д. Таким образом, вы можете определить дополнительный уровень логики для вашего TemperatureField.

...