- PVSM.RU - https://www.pvsm.ru -
Одна из новых возможностей, появившихся в Python 3.7 [1] — классы данных (Data classes). Они призваны автоматизировать генерацию кода классов, которые используются для хранения данных. Не смотря на то, что они используют другие механизмы работы, их можно сравнить с "изменяемыми именованными кортежами со значениями по-умолчанию".
Все приведенные примеры требуют для своей работы Python 3.7 или выше
Большинству 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
Важно отметить, что аннотации типов [4] обязательны. Все поля, которые не имеют отметок о типе будут проигнорированы. Конечно, если вы не хотите использовать конкретный тип, вы можете указать 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 = {'title': 'Fahrenheit 451', 'author': 'Bradbury'}
Однако у такого подхода есть недостатки:
{'name': 'Fahrenheit 451', 'author': 'Bradbury'}
тоже будет формально корректной. Есть вариант получше:
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 [5]. Он умеет даже больше чем dataclass и работает на более старых версиях python таких как 2.7 и 3.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
, чтобы оно стало неизменяемым. Этот параметр не используется самими классами данных и предназначено для работы сторонних расширений.Автосгенерированный метод __init__
вызывает метод __post_init__
, если он определен в классе. Как правило он вызывается в форме self.__post_init__()
, однако если в классе определены переменные типа InitVar
, они будут переданы в качестве параметров метода.
Если метод __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')
Одна из возможностей, связанных с методом __post_init__
— параметры, используемые только для инициализации. Если при объявления поля указать в качестве его типа InitVar
, его значение будет передано как параметр метода __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)
Автор: Tishka17
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/284675
Ссылки в тексте:
[1] Python 3.7: https://www.python.org/downloads/release/python-370/
[2] PEP 557 — Data classes: https://www.python.org/dev/peps/pep-0557/
[3] Официальная документация: https://docs.python.org/3/library/dataclasses.html
[4] аннотации типов: https://www.python.org/dev/peps/pep-0526/
[5] attrs: https://attrs.org
[6] Источник: https://habr.com/post/415829/?utm_source=habrahabr&utm_medium=rss&utm_campaign=415829
Нажмите здесь для печати.