Главная » Хабрахабр » Введение в Data classes

Введение в Data classes

7 — классы данных (Data classes). Одна из новых возможностей, появившихся в Python 3. Не смотря на то, что они используют другие механизмы работы, их можно сравнить с "изменяемыми именованными кортежами со значениями по-умолчанию". Они призваны автоматизировать генерацию кода классов, которые используются для хранения данных.

Введение

7 или выше Все приведенные примеры требуют для своей работы Python 3.

Большинству python-разработчикам приходится регулярно писать такие классы:

class RegularBook: def __init__(self, title, author): self.title = title self.author = author

Идентификаторы title и author используются несколько раз. Уже на этом примере видна избыточность. Реальный класс же будет ещё содержать переопределенные методы __eq__ и __repr__.

С его использованием аналогичный код будет выглядеть так: Модуль dataclasses содержит декоратор @dataclass.

@dataclass
class Book: title: str author: str

Все поля, которые не имеют отметок о типе будут проигнорированы. Важно отметить, что аннотации типов обязательны. Конечно, если вы не хотите использовать конкретный тип, вы можете указать Any из модуля typing.

Вы автоматически получаете класс, с реализованными методами __init__, __repr__, __str__ и __eq__. Что же вы получаете в результате? Кроме того, это будет обычный класс и вы можете наследоваться от него или добавлять произвольные методы.

>>> book = Book(title="Fahrenheit 451", author="Bradbury")
>>> book
Book(title='Fahrenheit 451', author='Bradbury')
>>> book.author 'Bradbury'
>>> other = Book("Fahrenheit 451", "Bradbury")
>>> book == other
True

Альтернативы

Кортеж или словарь

Конечно, если структура довольна простая, можно сохранить данные в словарь или кортеж:

book = ("Fahrenheit 451", "Bradbury")
other =

Однако у такого подхода есть недостатки:

  • Необходимо помнить, что переменная содержит данные, относящиеся к данной структуре.
  • В случае словаря, вы должны следить за названиями ключей. Такая инициализация словаря {'name': 'Fahrenheit 451', 'author': 'Bradbury'} тоже будет формально корректной.
  • В случае кортежа вы должны следить за порядком значений, так как они не имеют имен.

Есть вариант получше:

Namedtuple

from collections import namedtuple NamedTupleBook = namedtuple("NamedTupleBook", ["title", "author"])

Если мы воспользуемся классом, созданным таким образом, мы получим фактически то же самое, что и использованием с data class.

>>> book = NamedTupleBook("Fahrenheit 451", "Bradbury")
>>> book.author 'Bradbury'
>>> book
NamedTupleBook(title='Fahrenheit 451', author='Bradbury')
>>> book == NamedTupleBook("Fahrenheit 451", "Bradbury"))
True

Они происходят из того, что именованные кортежи все ещё являются кортежами. Но несмотря на общую схожесть, именованные кортежи имеют свои ограничения.

Во-первых, вы все ещё можете сравнивать экземпляры разных классов.

>>> Car = namedtuple("Car", ["model", "owner"])
>>> book = NamedTupleBook("Fahrenheit 451", "Bradbury"))
>>> book == Car("Fahrenheit 451", "Bradbury")
True

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

Другие проекты

В частности, проект attrs. Если не ограничиваться стандартной библиотекой, можно найти другие решения данной задачи. 7 и 3. Он умеет даже больше чем dataclass и работает на более старых версиях python таких как 2. И тем не менее, то, что он не является частью стандартной библиотеки, может быть неудобно 4.

Создание

В этом случае, все поля класса, определенные с аннотацией типов будут использоваться в соответствующих методах результирующего класса. Для создания класса данных можно воспользоваться декоратором @dataclass.

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

from dataclasses import make_dataclass
Book = make_dataclass("Book", ["title", "author"])
book = Book("Fahrenheit 451", "Bradbury")

Значения по-умолчанию

Все ещё не требуется переопределять метод __init__, достаточно указать значения прямо в классе. Одна из полезных особенностей — легкость добавления к полям значений по-умолчанию.

@dataclass
class Book: title: str = "Unknown" author: str = "Unknown author"

Они будут учтены в сгенерированном методе __init__

>>> Book()
Book(title='Unknown', author='Unknown author')
>>> Book("Farenheit 451")
Book(title='Farenheit 451', author='Unknown author')

Если вам, например, необходимо использовать список в качестве есть значения по-умолчанию, есть другой способ, но об этом ниже. Но как и в случае с обычными классами и методами надо быть аккуратным с использованием изменяемых значений по-умолчанию.

Кроме того, важно следить за порядком определения полей, имеющих значения по-умолчанию, так как он в точности соответствует их порядку в методе __init__

Иммутабельные классы данных

Во многих ситуациях, это хорошая идея. Экземпляры именованных кортежей неизменяемые. Просто укажите параметр frozen=True при создании класса и если вы попытаетесь изменять его поля, выбросится исключение FrozenInstanceError Для классов данных вы тоже можете сделать это.

@dataclass(frozen=True)
class Book: title: str author: str

>>> book = Book("Fahrenheit 451", "Bradbury")
>>> book.title = "1984"
dataclasses.FrozenInstanceError: cannot assign to field 'title'

Настройка класса данных

Кроме параметра frozen, декоратор @dataclass обладает другими параметрами:

  • init: если он равен True (по-умолчанию), генерируется метод __init__. Если у класса уже определен метод __init__, параметр игнорируется.
  • repr: включает (по-умолчанию) создание метода __repr__. Сгенерированная строка содержит имя класса и название и представление всех полей, определенных в классе. При этом можно исключить отдельные поля (см. ниже)
  • eq: включает (по-умолчанию) создание метода __eq__. Объекты сравниваются так же, как если бы это были кортежи, содержащие соответствующие значения полей. Дополнительно проверяется совпадение типов.
  • order включает (по-умолчанию выключен) создание методов __lt__, __le__, __gt__ и __ge__. Объекты сравниваются так же, как соответствующие кортежи из значений полей. При этом так же проверяется тип объектов. Если order задан, а eq — нет, будет сгенерировано исключение ValueError. Так же, класс не должен содержать уже определенных методов сравнения.
  • unsafe_hash влияет на генерацию метода __hash__. Поведение так же зависит от значений параметров eq и frozen

Настройка отдельных полей

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

Изменяемые значения по-умолчанию

Мы можете захотеть класс "книжная полка", содержащий список книг. Типичная ситуация, о которой говорилось выше — использование списков или других изменяемых значений по-умолчанию. Если вы запустите следующий код:

@dataclass
class Bookshelf: books: List[Book] = []

интерпретатор сообщит об ошибке:

ValueError: mutable default <class 'list'> for field books is not allowed: use default_factory

Однако для других изменяемых значений это предупреждение не сработает и приведет к некорректному поведению программы.

В качестве его значения может быть любой вызываемый объект или функция без параметров.
Корректная версия класса выглядит так: Чтобы избежать проблем, предлагается использовать параметр default_factory функции field.

@dataclass
class Bookshelf: books: List[Book] = field(default_factory=list)

Другие параметры

Кроме указанного default_factory функция field имеет следующие параметры:

  • default: значение по-умолчанию. Этот параметр необходим, так как вызов field заменяет задание значения поля по-умолчанию
  • init: включает (задан по-умолчанию) использование поля в методе __init__
  • repr: включает (задан по-умолчанию) использование поля в методе __repr__
  • compare включает (задан по-умолчанию) использование поля в методах сравнения (__eq__, __le__ и других)
  • hash: может быть булевое значение или None. Если он равен True, поле используется при вычислении хэша. Если указано None (по-умолчанию) — используется значение параметра compare.
    Одной из причин указать hash=False при заданном compare=True может быть сложность вычисления хэша поля при том, что оно необходимо для сравнения.
  • metadata: произвольный словарь или None. Значение оборачивается в MappingProxyType, чтобы оно стало неизменяемым. Этот параметр не используется самими классами данных и предназначено для работы сторонних расширений.

Обработка после инициализации

Как правило он вызывается в форме self.__post_init__(), однако если в классе определены переменные типа InitVar, они будут переданы в качестве параметров метода. Автосгенерированный метод __init__ вызывает метод __post_init__, если он определен в классе.

Если метод __init__ не был сгенерирован, то он __post_init__ не будет вызываться.

Например, добавим сгенерированное описание книги


@dataclass
class Book: title: str author: str desc: str = None def __post_init__(self): self.desc = self.desc or "`%s` by %s" % (self.title, self.author)

>>> Book("Fareneheit 481", "Bradbury")
Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury')

Параметры только для инициализации

Если при объявления поля указать в качестве его типа InitVar, его значение будет передано как параметр метода __post_init__. Одна из возможностей, связанных с методом __post_init__ — параметры, используемые только для инициализации. Никак по-другому такие поля не используются в классе данных.

@dataclass
class Book: title: str author: str gen_desc: InitVar[bool] = True desc: str = None def __post_init__(self, gen_desc: str): if gen_desc and self.desc is None: self.desc = "`%s` by %s" % (self.title, self.author)

>>> Book("Fareneheit 481", "Bradbury")
Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury')
>>> Book("Fareneheit 481", "Bradbury", gen_desc=False)
Book(title='Fareneheit 481', author='Bradbury', desc=None)

Наследование

Все сгенерированные методы используют поля из полученного упорядоченного словаря. Когда вы используете декоратор @dataclass, он проходит по всем родительским классам начиная с object и для каждого найденного класса данных сохраняет поля в упорядоченный словарь (ordered mapping), затем добавляя свойства обрабатываемого класса.

Как следствие, если родительский класс определяет значения по-умолчанию, вы должны будете поля определять со значениями по-умолчанию.

Так как упорядоченный словарь хранит значения в порядке вставки, то для следующих классов

@dataclass
class BaseBook: title: Any = None author: str = None @dataclass
class Book(BaseBook): desc: str = None title: str = "Unknown"

будет сгенерирован __init__ метод с такой сигнатурой:

def __init__(self, title: str="Unknown", author: str=None, desc: str=None)


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

В Германии разработали требования к домашним маршрутизаторам

Продолжительное время в Интернете регулярно появляются статьи об уязвимости маршрутизаторов для SOHO сегмента. Я тоже публиковал статью как обнаружить, что Ваш Микротик взломан. Резкий рост участников нашего канала в Телеграм (@router_os) показал, что проблема крайне остра. Но проблема стоит глобальней. ...

[Перевод] Архитектуры нейросетей

Перевод Neural Network Architectures Давайте рассмотрим историю их развития за последние несколько лет. Алгоритмы глубоких нейросетей сегодня обрели большую популярность, которая во многом обеспечивается продуманностью архитектур. Если вас интересует более глубокий анализ, обратитесь к этой работе. Подробнее здесь. Сравнение популярных ...