Как создавать пользовательские поля модели
Введение
Документация по моделям базы данных объясняет, как использовать стандартные типы полей Django – CharField, DateField и т.д. Для многих задач этих типов достаточно. Однако иногда стандартных типов Django недостаточно для ваших нужд, или вам требуется поле, которое полностью отличается от полей, поставляемых с Django.
Встроенные типы полей Django не охватывают все возможные типы столбцов базы данных – только распространенные типы, такие как VARCHAR и INTEGER. Для менее распространённых типов столбцов, таких как географические полигоны или даже пользовательские типы, например, пользовательские типы PostgreSQL, вы можете определить свои собственные подклассы Django Field.
В качестве альтернативы, у вас может быть сложный объект Python, который можно каким-то образом сериализовать для хранения в стандартном типе столбца базы данных. В этом случае подкласс Field поможет вам использовать ваш объект в ваших моделях.
Наш пример объекта
Создание пользовательских полей требует внимательности к деталям. Чтобы упростить понимание, мы будем использовать последовательный пример в этом документе: обертывание объекта Python, представляющего раздачу карт в игре бридж. Не беспокойтесь, вам не нужно знать, как играть в бридж, чтобы понять этот пример. Вам нужно знать только то, что 52 карты раздаются поровну четырём игрокам, традиционно называемым север, восток, юг и запад. Наш класс выглядит примерно так:
class Hand:
"""A hand of cards (bridge style)"""
def __init__(self, north, east, south, west):
# Input parameters are lists of cards ('Ah', '9s', etc.)
self.north = north
self.east = east
self.south = south
self.west = west
# ... (other possibly useful methods omitted) ...
Это обычный класс Python, без чего-либо специфичного для Django. Мы хотели бы иметь возможность делать такие вещи в наших моделях (мы предполагаем, что атрибут hand в модели является экземпляром Hand):
example = MyModel.objects.get(pk=1) print(example.hand.north) new_hand = Hand(north, east, south, west) example.hand = new_hand example.save()
Мы обращаемся к атрибуту hand в нашей модели так же, как и к любому другому классу Python. Секрет в том, чтобы указать Django, как обрабатывать сохранение и загрузку такого объекта.
Для использования класса Hand в наших моделях мы не должны изменять этот класс. Это идеально, так как позволяет легко добавить поддержку моделей к существующим классам, исходный код которых изменить нельзя.
Примечание
Возможно, вам нужно только воспользоваться пользовательскими типами столбцов базы данных и работать с данными как со стандартными типами Python в ваших моделях; строки или числа с плавающей точкой, например. Этот случай аналогичен нашему примеру Hand и мы отметим любые различия по ходу дела.
Теоретические основы
Хранение в базе данных
Начнём с полей модели. Если разложить всё по полочкам, поле модели предоставляет способ взять обычный объект Python – строку, булево значение, datetime, или что-то более сложное, например Hand, и преобразовать его в формат, удобный для работы с базой данных. (Такой формат также полезен для сериализации, но, как мы увидим позже, это проще, когда база данных настроена).
Поля в модели должны каким-то образом преобразовываться для соответствия существующему типу столбца базы данных. Разные базы данных предлагают разные наборы допустимых типов столбцов, но правило остаётся тем же: это единственные типы, с которыми вам нужно работать. Всё, что вы хотите сохранить в базе данных, должно соответствовать одному из этих типов.
Обычно вы либо создаёте поле Django, соответствующее определённому типу столбца базы данных, либо вам потребуется способ преобразования ваших данных, скажем, в строку.
Для нашего примера Hand мы можем преобразовать данные о картах в строку из 104 символов, объединив все карты в определённом порядке – например, сначала все карты севера, затем востока, юга и запада. Таким образом, объекты Hand могут быть сохранены в текстовых или символьных столбцах базы данных.
Что делает класс поля?
Все поля Django (когда в этом документе мы говорим о «полях», мы всегда имеем в виду поля модели, а не поля формы) являются подклассами django.db.models.Field. Большая часть информации, которую Django записывает о поле, общая для всех полей – имя, пояснение, уникальность и так далее. Хранение всей этой информации обрабатывается классом Field. Мы подробнее рассмотрим, что может делать Field позже; пока достаточно сказать, что всё происходит от Field и затем настраивает ключевые части поведения класса.
Важно понимать, что класс поля Django не хранится в ваших атрибутах модели. Атрибуты модели содержат обычные объекты Python. Классы полей, которые вы определяете в модели, фактически хранятся в классе Meta при создании класса модели (точные детали того, как это делается, здесь не важны). Это потому, что классы полей не нужны, когда вы просто создаёте и изменяете атрибуты. Вместо этого они предоставляют механизм преобразования между значением атрибута и тем, что хранится в базе данных или отправляется в сериализатор.
Помните об этом, когда создаёте свои пользовательские поля. Подкласс Django Field который вы пишете, обеспечивает механизм преобразования между вашими экземплярами Python и значениями базы данных/сериализатора различными способами (например, существуют различия между хранением значения и использованием значения для поиска). Если это кажется немного сложным, не волнуйтесь – всё станет яснее в примерах ниже. Просто помните, что вы часто будете создавать два класса, когда хотите иметь пользовательское поле:
- Первый класс – это объект Python, с которым будут взаимодействовать ваши пользователи. Они будут назначать его атрибуту модели, читать из него для отображения и так далее. Это класс
Handв нашем примере. - Второй класс – это подкласс
Field. Этот класс знает, как преобразовать ваш первый класс в постоянную форму хранения и обратно в форму Python.
Написание подкласса поля
При планировании подкласса Field, сначала подумайте, какой существующий класс Field наиболее похож на ваше новое поле. Можете ли вы создать подкласс существующего поля Django, чтобы сэкономить время? Если нет, вы должны создать подкласс класса Field, от которого всё унаследовано.
Инициализация вашего нового поля сводится к отделению аргументов, специфичных для вашего случая, от общих аргументов и передаче последних методу __init__() класса Field (или вашего родительского класса).
В нашем примере мы назовём наше поле HandField. (Хорошая идея – называть подкласс Field так, чтобы он легко идентифицировался как подкласс Field.) Оно не ведёт себя как любое существующее поле, поэтому мы создадим подкласс непосредственно от Field:
from django.db import models
class HandField(models.Field):
description = "A hand of cards (bridge style)"
def __init__(self, *args, **kwargs):
kwargs["max_length"] = 104
super().__init__(*args, **kwargs)
Наш класс HandField принимает большинство стандартных параметров поля (см. список ниже), но мы гарантируем, что он имеет фиксированную длину, поскольку он должен содержать только 52 значения карт и их масти; всего 104 символа.
Примечание
Многие поля моделей Django принимают параметры, которые они не используют. Например, вы можете передать как editable, так и auto_now в поле django.db.models.DateField, и оно проигнорирует параметр editable (auto_now устанавливается, что подразумевает editable=False). В этом случае ошибка не возникает.
Это поведение упрощает классы полей, потому что им не нужно проверять параметры, которые не нужны. Они передают все параметры родительскому классу, а затем не используют их. Вам решать, хотите ли вы, чтобы ваши поля были более строгими относительно выбранных параметров или использовать более допускающее поведение текущих полей.
Метод Field.__init__() принимает следующие параметры:
verbose_namenameprimary_keymax_lengthuniqueblanknulldb_index-
rel: Используется для связанных полей (например,ForeignKey). Только для расширенного использования. defaulteditable-
serialize: ЕслиFalse, поле не будет сериализовано, когда модель передаётся в сериализаторы Django сериализаторы. По умолчаниюTrue. unique_for_dateunique_for_monthunique_for_yearchoiceshelp_textdb_column-
db_tablespace: Только для создания индексов, если бэкенд поддерживает пространства таблиц. Обычно этот параметр можно игнорировать. -
auto_created:Trueесли поле было автоматически создано, как дляOneToOneField, используемого наследованием модели. Только для расширенного использования.
Все параметры без пояснения в вышеприведённом списке имеют то же значение, что и для обычных полей Django. Смотрите документацию по полям для примеров и подробностей.
Деконструкция поля
Противоположностью написанию вашего метода __init__() является написание метода deconstruct(). Он используется во время миграций моделей, чтобы указать Django, как преобразовать экземпляр вашего нового поля в сериализованную форму — в частности, какие аргументы передать в __init__() для его восстановления.
Если вы не добавили дополнительных параметров поверх поля, унаследованного от базового, то нет необходимости писать новый метод deconstruct(). Однако, если вы изменяете аргументы, передаваемые в __init__() (как мы делаем в HandField), вам нужно дополнить передаваемые значения.
deconstruct() возвращает кортеж из четырёх элементов: имя атрибута поля, полный путь импорта класса поля, позиционные аргументы (в виде списка) и именованные аргументы (в виде словаря). Обратите внимание, что это отличается от метода deconstruct() для пользовательских классов, который возвращает кортеж из трёх элементов.
Как автору пользовательского поля, вам не нужно беспокоиться о первых двух значениях; базовый класс Field содержит весь код для определения имени атрибута поля и пути импорта. Однако вам нужно учитывать позиционные и именованные аргументы, так как, скорее всего, именно их вы изменяете.
Например, в нашем классе HandField мы всегда принудительно устанавливаем max_length в __init__(). Метод deconstruct() в базовом классе Field увидит это и попытается вернуть его в именованных аргументах; таким образом, мы можем исключить его из именованных аргументов для лучшей читаемости:
from django.db import models
class HandField(models.Field):
def __init__(self, *args, **kwargs):
kwargs["max_length"] = 104
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
del kwargs["max_length"]
return name, path, args, kwargs
Если вы добавляете новый именованный аргумент, вам нужно написать код в deconstruct() , который поместит его значение в kwargs самостоятельно. Вы также должны исключить значение из kwargs, когда это не требуется для восстановления состояния поля, например, когда используется значение по умолчанию:
from django.db import models
class CommaSepField(models.Field):
"Implements comma-separated storage of lists"
def __init__(self, separator=",", *args, **kwargs):
self.separator = separator
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
# Only include kwarg if it's not the default
if self.separator != ",":
kwargs["separator"] = self.separator
return name, path, args, kwargs
Более сложные примеры выходят за рамки этого документа, но помните — для любой конфигурации вашего экземпляра поля deconstruct() должен возвращать аргументы, которые вы можете передать в __init__ для восстановления этого состояния.
Обратите особое внимание, если вы устанавливаете новые значения по умолчанию для аргументов в суперклассе Field; вы хотите убедиться, что они всегда включены, а не исчезают, если принимают старое значение по умолчанию.
Кроме того, старайтесь избегать возвращения значений в качестве позиционных аргументов; по возможности возвращайте значения в качестве именованных аргументов для максимальной совместимости в будущем. Если вы чаще меняете имена, чем их позицию в списке аргументов конструктора, вы можете предпочесть позиционные, но помните, что люди будут восстанавливать ваше поле из сериализованной версии довольно долго (возможно, годами), в зависимости от срока жизни ваших миграций.
Вы можете увидеть результаты деконструкции, посмотрев в миграции, которые включают поле, а также можете протестировать деконструкцию в юнит-тестах, деконструируя и восстанавливая поле:
name, path, args, kwargs = my_field_instance.deconstruct() new_instance = MyField(*args, **kwargs) self.assertEqual(my_field_instance.some_attribute, new_instance.some_attribute)
Атрибуты поля, не влияющие на определение столбца базы данных
Вы можете переопределить Field.non_db_attrs для настройки атрибутов поля, которые не влияют на определение столбца. Это используется во время миграций моделей для обнаружения операций AlterField без изменений.
Например:
class CommaSepField(models.Field):
@property
def non_db_attrs(self):
return super().non_db_attrs + ("separator",)
Изменение базового класса пользовательского поля
Вы не можете изменить базовый класс пользовательского поля, потому что Django не обнаружит изменения и не создаст миграцию для этого. Например, если вы начинаете с:
class CustomCharField(models.CharField): ...
и затем решите, что хотите использовать TextField вместо, вы не можете изменить подкласс так:
class CustomCharField(models.TextField): ...
Вместо этого, вы должны создать новый класс пользовательского поля и обновить свои модели, чтобы они ссылались на него:
class CustomCharField(models.CharField): ... class CustomTextField(models.TextField): ...
Как обсуждалось в удалении полей, вы должны сохранить исходный класс CustomCharField до тех пор, пока у вас есть миграции, которые на него ссылаются.
Документирование вашего пользовательского поля
Как всегда, вы должны задокументировать тип поля, чтобы пользователи знали, что это такое. Помимо предоставления для него docstring, что полезно для разработчиков, вы также можете позволить пользователям приложения администрирования увидеть краткое описание типа поля через приложение django.contrib.admindocs. Для этого укажите описательный текст в атрибуте description вашего пользовательского поля. В приведенном выше примере описание, отображаемое приложением admindocs для HandField будет «Колода карт (в стиле бридж)».
В представлении приложения django.contrib.admindocs, описание поля интерполируется с помощью field.__dict__, что позволяет описанию включать аргументы поля. Например, описание для CharField такое:
description = _("String (up to %(max_length)s)")
Полезные методы
После создания подкласса Field, вы можете рассмотреть возможность переопределения нескольких стандартных методов, в зависимости от поведения вашего поля. Список методов ниже приблизительно в порядке убывания важности, поэтому начните с верхних.
Пользовательские типы баз данных
Допустим, вы создали пользовательский тип PostgreSQL под названием mytype. Вы можете наследоваться от Field и реализовать метод db_type(), как показано ниже:
from django.db import models
class MytypeField(models.Field):
def db_type(self, connection):
return "mytype"
После создания MytypeField, вы можете использовать его в любой модели, как любой другой тип Field:
class Person(models.Model):
name = models.CharField(max_length=80)
something_else = MytypeField()
Если вы стремитесь создать приложение, независимое от базы данных, вы должны учитывать различия в типах столбцов баз данных. Например, тип столбца даты/времени в PostgreSQL называется timestamp, а в MySQL — datetime. Вы можете обработать это в методе db_type() , проверив атрибут connection.vendor. Текущие встроенные имена поставщиков: sqlite, postgresql, mysql, и oracle.
Например:
class MyDateField(models.Field):
def db_type(self, connection):
if connection.vendor == "mysql":
return "datetime"
else:
return "timestamp"
Методы db_type() и rel_db_type() вызываются Django при построении CREATE TABLE-выражений для вашего приложения — то есть, при первом создании ваших таблиц. Эти методы также вызываются при построении WHERE-запроса, который включает поле модели — то есть, при получении данных с помощью методов QuerySet, таких как get(), filter(), и exclude(), и когда поле модели используется в качестве аргумента.
Некоторые типы столбцов базы данных принимают параметры, например, CHAR(25), где параметр 25 представляет максимальную длину столбца. В таких случаях гибче указать параметр в модели, а не в коде метода db_type(). Например, не имеет смысла иметь CharMaxlength25Field, показанный здесь:
# This is a silly example of hard-coded parameters.
class CharMaxlength25Field(models.Field):
def db_type(self, connection):
return "char(25)"
# In the model:
class MyModel(models.Model):
# ...
my_field = CharMaxlength25Field()
Лучший способ сделать это — сделать параметр изменяемым во время выполнения — то есть, при создании экземпляра класса. Для этого реализуйте метод Field.__init__(), как показано ниже:
# This is a much more flexible example.
class BetterCharField(models.Field):
def __init__(self, max_length, *args, **kwargs):
self.max_length = max_length
super().__init__(*args, **kwargs)
def db_type(self, connection):
return "char(%s)" % self.max_length
# In the model:
class MyModel(models.Model):
# ...
my_field = BetterCharField(25)
Наконец, если ваш столбец требует действительно сложной настройки SQL, верните значение None из db_type(). Это заставит код Django по созданию SQL пропустить это поле. Вам затем нужно будет создать столбец в нужной таблице другим способом, но это даст вам возможность указать Django обойти это поле.
Метод rel_db_type() вызывается полями, такими как ForeignKey и OneToOneField, которые указывают на другое поле, для определения типов данных столбцов базы данных. Например, если у вас есть UnsignedAutoField, вам также нужны внешние ключи, которые указывают на это поле, чтобы использовать тот же тип данных:
# MySQL unsigned integer (range 0 to 4294967295).
class UnsignedAutoField(models.AutoField):
def db_type(self, connection):
return "integer UNSIGNED AUTO_INCREMENT"
def rel_db_type(self, connection):
return "integer UNSIGNED"
Преобразование значений в объекты Python
Если ваш пользовательский класс Field работает с структурами данных, более сложными, чем строки, даты, целые числа или числа с плавающей точкой, то вам может потребоваться переопределить from_db_value() и to_python().
Если для подкласса поля присутствует метод from_db_value(), он будет вызываться во всех случаях, когда данные загружаются из базы данных, включая агрегации и вызовы values().
to_python() вызывается при десериализации и во время метода clean(), используемого из форм.
В общем случае, метод to_python() должен корректно обрабатывать следующие аргументы:
- Экземпляр соответствующего типа (например,
Handв нашем примере). - Строку
-
None(если поле допускаетnull=True)
В нашем классе HandField, мы храним данные в поле VARCHAR в базе данных, поэтому нам нужно уметь обрабатывать строки и None в методе from_db_value(). В to_python(), нам также необходимо обрабатывать экземпляры Hand.
import re
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
def parse_hand(hand_string):
"""Takes a string of cards and splits into a full hand."""
p1 = re.compile(".{26}")
p2 = re.compile("..")
args = [p2.findall(x) for x in p1.findall(hand_string)]
if len(args) != 4:
raise ValidationError(_("Invalid input for a Hand instance"))
return Hand(*args)
class HandField(models.Field):
# ...
def from_db_value(self, value, expression, connection):
if value is None:
return value
return parse_hand(value)
def to_python(self, value):
if isinstance(value, Hand):
return value
if value is None:
return value
return parse_hand(value)
Обратите внимание, что из этих методов всегда возвращается экземпляр Hand. Это тип объекта Python, который мы хотим сохранить в атрибуте модели.
Для to_python(), если во время преобразования значения возникнут какие-либо проблемы, необходимо вызывать исключение ValidationError.
Преобразование объектов Python в значения запросов
Поскольку использование базы данных требует преобразования в обоих направлениях, если вы переопределите from_db_value(), то вы также должны переопределить get_prep_value(), чтобы преобразовать объекты Python обратно в значения запросов.
Например:
class HandField(models.Field):
# ...
def get_prep_value(self, value):
return "".join(
["".join(l) for l in (value.north, value.east, value.south, value.west)]
)
Предупреждение
Если ваш пользовательский тип поля использует типы CHAR, VARCHAR или TEXT для MySQL, убедитесь, что get_prep_value() всегда возвращает строковый тип. MySQL выполняет гибкое и неожиданное сопоставление при выполнении запроса на эти типы, и если предоставленное значение является целым числом, что может привести к включению неожиданных объектов в результаты запроса. Эта проблема не может возникнуть, если вы всегда возвращаете строковый тип из get_prep_value().
Преобразование значений запросов в значения базы данных
Некоторые типы данных (например, даты) должны быть в определенном формате, прежде чем они могут быть использованы бэкендом базы данных. get_db_prep_value() — это метод, где следует проводить эти преобразования. Специфическое подключение, которое будет использоваться для запроса, передается в качестве параметра connection. Это позволяет использовать бэкенд-специфичную логику преобразования, если это необходимо.
Например, Django использует следующий метод для своего BinaryField:
def get_db_prep_value(self, value, connection, prepared=False):
value = super().get_db_prep_value(value, connection, prepared)
if value is not None:
return connection.Database.Binary(value)
return value
В случае, если вашему пользовательскому полю требуется специальное преобразование при сохранении, отличное от преобразования, используемого для обычных параметров запроса, вы можете переопределить get_db_prep_save().
Предварительная обработка значений перед сохранением
Если вы хотите обработать значение непосредственно перед сохранением, используйте pre_save(). Например, Django использует этот метод для корректной установки атрибута в случае DateTimeField при auto_now или auto_now_add.
Если вы переопределяете этот метод, вы должны вернуть значение атрибута в конце. Также необходимо обновить атрибут модели, если вы внесли какие-либо изменения в значение, чтобы код, содержащий ссылки на модель, всегда видел правильное значение.
Настройка поля формы для поля модели
Для настройки поля формы, используемого ModelForm, вы можете переопределить formfield().
Класс поля формы можно указать с помощью аргументов form_class и choices_form_class; последний используется, если для поля указаны варианты, а первый — в противном случае. Если эти аргументы не указаны, будут использоваться CharField или TypedChoiceField.
Весь словарь kwargs передается напрямую в метод __init__() поля формы. Как правило, вам нужно только установить хорошее значение по умолчанию для аргумента form_class (и, возможно, choices_form_class) и делегировать дальнейшую обработку родительскому классу. Это может потребовать написания пользовательского поля формы (и даже виджета формы). См. документацию по формам для получения дополнительной информации.
Продолжая наш пример, мы можем написать метод formfield() как:
class HandField(models.Field):
# ...
def formfield(self, **kwargs):
# This is a fairly standard way to set up some defaults
# while letting the caller override them.
defaults = {"form_class": MyFormField}
defaults.update(kwargs)
return super().formfield(**defaults)
Предполагается, что мы импортировали класс поля MyFormField (который имеет свой собственный виджет по умолчанию). Этот документ не описывает подробно написание пользовательских полей формы.
Эмуляция встроенных типов полей
Если вы создали метод db_type(), вам не нужно беспокоиться о методе get_internal_type() — он вряд ли будет использоваться. Иногда, однако, хранение в вашей базе данных похоже на хранение в другом поле, поэтому вы можете использовать логику другого поля, чтобы создать правильный столбец.
Например:
class HandField(models.Field):
# ...
def get_internal_type(self):
return "CharField"
Независимо от того, какой бэкенд базы данных мы используем, это означает, что migrate и другие SQL-команды создают правильный тип столбца для хранения строки.
Если get_internal_type() возвращает строку, которая неизвестна Django для используемого вами бэкенда базы данных – то есть, она не появляется в django.db.backends.<db_name>.base.DatabaseWrapper.data_types – эта строка всё равно будет использована сериализатором, но метод по умолчанию db_type() вернёт None. Обратитесь к документации db_type() для понимания причин, по которым это может быть полезно. Вставка описательной строки в качестве типа поля для сериализатора полезна, если вы планируете использовать вывод сериализатора где-то ещё, вне Django.
Преобразование данных поля модели для сериализации
Для настройки способа сериализации значений сериализатором можно переопределить value_to_string(). Лучший способ получить значение поля до сериализации – использование value_from_object(). Например, поскольку HandField использует строки для хранения данных, мы можем повторно использовать имеющийся код преобразования:
class HandField(models.Field):
# ...
def value_to_string(self, obj):
value = self.value_from_object(obj)
return self.get_prep_value(value)
Общие рекомендации
Создание пользовательского поля может быть сложным процессом, особенно при выполнении сложных преобразований между вашими типами Python и форматами базы данных и сериализации. Вот несколько советов, которые помогут сделать процесс проще:
- Изучите существующие поля Django (в django/db/models/fields/__init__.py) для вдохновения. Попробуйте найти похожее поле и немного его расширить, вместо создания нового поля с нуля.
- Добавьте метод
__str__()в класс, который вы используете как поле. Есть много мест, где по умолчанию код поля вызываетstr()для значения. (В наших примерах в этом документеvalueбудет экземпляромHand, а неHandField). Поэтому, если ваш метод__str__()автоматически преобразует объект Python в строковое представление, вы сможете сэкономить много времени.
Создание подкласса FileField
Помимо вышеперечисленных методов, поля, работающие с файлами, имеют несколько дополнительных специальных требований, которые необходимо учитывать. Большая часть механизмов, предоставляемых FileField, таких как управление хранением и извлечением из базы данных, могут остаться без изменений, оставив подклассам задачу поддержки определённого типа файла.
Django предоставляет класс File, который используется в качестве прокси для содержимого и операций с файлом. Его можно расширить, чтобы настроить доступ к файлу и доступные методы. Он находится в django.db.models.fields.files, а его поведение по умолчанию описано в документации по файлам.
После создания подкласса File, новый подкласс FileField должен быть проинформирован об использовании этого подкласса. Для этого нужно назначить новый подкласс File специальному атрибуту attr_class подкласса FileField.
Несколько предложений
Помимо вышеизложенного, есть несколько рекомендаций, которые значительно улучшат эффективность и читаемость кода поля.
- Исходный код собственного
ImageFieldDjango (в django/db/models/fields/files.py) является отличным примером того, как подклассироватьFileFieldдля поддержки определённого типа файла, так как он включает в себя все описанные выше методы. - Кэшируйте атрибуты файлов, где это возможно. Поскольку файлы могут храниться в удалённых системах хранения, их извлечение может занимать дополнительное время или даже дополнительные расходы, которые не всегда необходимы. После извлечения файла для получения некоторых данных о его содержимом, кэшируйте как можно больше этих данных, чтобы уменьшить количество извлечений файла при последующих вызовах для этой информации.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/5.1/howto/custom-model-fields/