Собственные поля


Вступление
В документации model reference объясняется, как использовать стандартные классы полей Django – CharField, DateField и т.д. Для многих целей эти классы - все, что вам нужно. Иногда, однако, версия Django не отвечает вашим точным требованиям, или вы захотите использовать поле, которое полностью отличается от тех, что поставляются с Django.

Встроенные типы полей Django не охватывают все возможные типы столбцов баз данных - только распространенные типы, такие как VARCHAR и INTEGER. Для более непонятных типов столбцов, таких как географические полигоны или даже созданных пользователем типов, таких как PostgreSQL custom types, вы можете определить свои собственные подклассы 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 (и когда мы говорим поля в этом документе, мы всегда имеем в виду поля модели, а не form fields) являются подклассами django.db.models.Field. Большая часть информации, которую Django записывает о поле, является общей для всех полей - имя, текст справки, уникальность и так далее. Хранением всей этой информации занимается Field. Позже мы подробнее рассмотрим, что может делать Field; сейчас достаточно сказать, что все происходит от Field и затем настраивает ключевые части поведения класса.

Важно понимать, что класс поля Django - это не то, что хранится в атрибутах вашей модели. Атрибуты модели содержат обычные объекты Python. Классы полей, которые вы определяете в модели, на самом деле хранятся в классе Meta при создании класса модели (точные детали того, как это делается, здесь не важны). Это происходит потому, что классы полей не нужны, когда вы просто создаете и изменяете атрибуты. Вместо этого они обеспечивают механизм преобразования между значением атрибута и тем, что хранится в базе данных или отправляется в serializer.

Помните об этом при создании собственных пользовательских полей. Подкласс Django Field, который вы пишете, предоставляет механизм для преобразования между вашими экземплярами Python и значениями базы данных/сериализатора различными способами (есть различия между хранением значения и использованием значения для поиска, например). Если это звучит немного сложно, не волнуйтесь - в примерах ниже все станет понятнее. Просто помните, что вы часто будете создавать два класса, когда вам нужно пользовательское поле:

Первый класс - это объект Python, которым будут манипулировать ваши пользователи. Они будут присваивать ему атрибут модели, читать из него для отображения, и тому подобное. В нашем примере это класс Hand.
Второй класс - это подкласс Field. Это класс, который знает, как преобразовать ваш первый класс туда и обратно между его формой постоянного хранения и формой Python.
Написание подкласса поля
Планируя свой подкласс Field, сначала подумайте, на какой из существующих классов Field ваше новое поле больше всего похоже. Можете ли вы подклассифицировать существующее поле Django и сэкономить себе немного работы? Если нет, то вам следует подклассифицировать класс Field, от которого все происходит.

Инициализация вашего нового поля - это вопрос отделения любых аргументов, специфичных для вашего случая, от общих аргументов и передачи последних методу __init__() Field (или вашему родительскому классу).

В нашем примере мы назовем наше поле HandField. (Хорошая идея - назвать свой подкласс Field <Something>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_name
name
primary_key
max_length
unique
blank
null
db_index
rel: Используется для связанных полей (как ForeignKey). Только для расширенного использования.
default
editable
serialize: Если False, поле не будет сериализовано, когда модель передается в serializers Django. По умолчанию True.
unique_for_date
unique_for_month
unique_for_year
choices
help_text
db_column
db_tablespace: Только для создания индекса, если бэкенд поддерживает tablespaces. Обычно эту опцию можно игнорировать.
auto_created: True, если поле было создано автоматически, как для OneToOneField, используемого при наследовании модели. Только для расширенного использования.
Все опции без пояснений в приведенном выше списке имеют то же значение, что и для обычных полей Django. Примеры и подробности смотрите в field documentation.

Деконструкция поля
Противоположностью написания метода __init__() является написание метода deconstruct(). Он используется во время model migrations, чтобы сообщить Django, как взять экземпляр вашего нового поля и уменьшить его до сериализованной формы - в частности, какие аргументы передать в __init__() для его повторного создания.

Если вы не добавили никаких дополнительных опций поверх поля, от которого вы наследуете, то нет необходимости писать новый метод deconstruct(). Если же вы изменяете аргументы, передаваемые в __init__() (как мы это делаем в HandField), то вам потребуется дополнить передаваемые значения.

deconstruct() возвращает кортеж из четырех элементов: имя атрибута поля, полный путь импорта класса поля, позиционные аргументы (в виде списка) и аргументы ключевых слов (в виде диктанта). Обратите внимание, что это отличается от метода deconstruct() for custom classes, который возвращает кортеж из трех элементов.

Как автору пользовательского поля, вам не нужно заботиться о первых двух значениях; базовый класс 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
Более сложные примеры выходят за рамки данного документа, но помните - для любой конфигурации вашего экземпляра Field, 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):
...
Как обсуждалось в removing fields, вы должны сохранять исходный класс 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(), и имеете поле модели в качестве аргумента. Они не вызываются в любое другое время, поэтому он может позволить себе выполнить немного сложный код, такой как проверка 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 в значения запроса
Поскольку использование базы данных требует преобразования в обе стороны, если вы переопределите 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), а затем делегировать дальнейшую обработку родительскому классу. Это может потребовать от вас написания пользовательского поля формы (и даже виджета формы). Смотрите 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 documentation.

После создания подкласса File новому подклассу FileField необходимо указать на необходимость его использования. Для этого нужно присвоить новому подклассу File специальный атрибут attr_class подкласса FileField.

Несколько предложений
В дополнение к вышеперечисленным деталям, есть несколько рекомендаций, которые могут значительно улучшить эффективность и читабельность кода поля.

Исходный текст собственного ImageField (в django/db/models/fields/files.py) Django является отличным примером того, как можно подклассифицировать FileField для поддержки определенного типа файлов, поскольку он включает в себя все описанные выше приемы.
По возможности кэшируйте атрибуты файлов. Поскольку файлы могут храниться в удаленных системах хранения, их извлечение может потребовать дополнительного времени или даже денег, которые не всегда необходимы. После извлечения файла для получения некоторых данных о его содержимом, кэшируйте как можно больше этих данных, чтобы уменьшить количество последующих обращений к файлу за этой информацией.