Написание пользовательских полей модели
Введение
Документация справочника по моделям объясняет, как использовать стандартные классы полей 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 в модели является экземпляром %%%CODE_BLOCK_8%%):
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). В этом случае ошибка не возникает.
Это поведение упрощает классы полей, так как им не нужно проверять параметры, которые не нужны. Они просто передают все параметры родительскому классу, а затем не используют их позже. Решать вам, хотите ли вы, чтобы ваши поля были более строгими в отношении выбора параметров или использовать более простое, более permissive поведение существующих полей.
Метод 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
Если вы добавляете новый именованный аргумент, вам необходимо написать код, чтобы поместить его значение в 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)
Изменение базового класса пользовательского поля
Вы не можете изменить базовый класс пользовательского поля, потому что Django не обнаружит изменение и не создаст миграцию для него. Например, если вы начинаете с:
class CustomCharField(models.CharField):
...
и затем решаете использовать TextField вместо него, вы не можете изменить подкласс так:
class CustomCharField(models.TextField):
...
Вместо этого необходимо создать новый класс пользовательского поля и обновить ваши модели для ссылки на него:
class CustomCharField(models.CharField):
...
class CustomTextField(models.TextField):
...
Как обсуждалось в удалении полей, вы должны сохранить исходный класс CustomCharField до тех пор, пока у вас есть миграции, которые ссылаются на него.
Документирование пользовательского поля
Как всегда, вы должны документировать тип вашего поля, чтобы пользователи понимали, что это такое. В дополнение к предоставлению документации для него, что полезно для разработчиков, вы также можете позволить пользователям приложения админ-панели увидеть краткое описание типа поля с помощью приложения django.contrib.admindocs. Для этого просто предоставьте описательный текст в атрибуте description вашего пользовательского поля. В приведенном выше примере описание, отображаемое приложением admindocs для HandField будет ‘A hand of cards (bridge style)’.
В отображении приложения 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.settings_dict['ENGINE'].
Например:
class MyDateField(models.Field):
def db_type(self, connection):
if connection.settings_dict['ENGINE'] == 'django.db.backends.mysql':
return 'datetime'
else:
return 'timestamp'
Методы db_type() и rel_db_type() вызываются Django при построении CREATE TABLE-заявлений для вашего приложения — то есть при первом создании таблиц. Методы также вызываются при построении WHERE-запроса, который включает поле модели — то есть при получении данных с помощью методов QuerySet, таких как get(), filter(), и exclude(), и имеют поле модели в качестве аргумента. Они не вызываются в другое время, поэтому вы можете позволить себе выполнить немного более сложный код, например, проверку connection.settings_dict в приведенном выше примере.
Некоторые типы столбцов базы данных принимают параметры, такие как 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(). Это заставит код создания SQL Django пропустить этот столбец. Затем вы будете отвечать за создание столбца в правильной таблице каким-либо другим способом, но это даст вам возможность сказать 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 в значения запросов
Поскольку использование базы данных требует преобразования в обоих направлениях, если вы переопределяете to_python(), вы также должны переопределить 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 использует этот метод для корректного задания атрибута в случае 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/2.0/howto/custom-model-fields/