В программировании на Python есть много случаев, когда может понадобиться больше одной переменной для представления определенного объекта.
Например, для представления одной книги, в книжном магазине, необходимо указать название книги, год издания, авторов (их может быть несколько), количество экземпляров этой книги, цена за единицу, общую стоимость всех книг или любую другую информацию:
book_title = 'Название книги' book_date = '01-01-2019' book_author = ['Автор 1', 'Автор 2'] book_price = 256 book_num = 9 book_total = book_price * book_num
Теперь есть шесть отдельных независимых переменных. Если надо будет передавать информацию о книге в какую-то функцию для дальнейшей обработки, то придется передавать каждую переменную по отдельности. Кроме того, если необходимо хранить информацию о какой-то еще книге, то придется дополнительно объявить еще шесть переменных! Такая реализация не очень эффективна и вообще, в итоге можно запутаться.
Python позволяет программистам, с помощью обычных классов, создавать свои собственные пользовательские типы данных, которые предназначены для упорядоченного хранения нестандартных данных. Проще говоря, группируют несколько отдельных переменных разных типов в единое целое.
Пользовательские типы данных создаются программистом с использованием синтаксиса класса:
class Book: def __init__(self, title=None, date=None, authors=None, price=0, num=0): self.title = title self.date = date self.price = price self.num = num self.total = self.price * self.num if authors is None: self.authors = [] else: self.authors = authors def __repr__(self): return f'{self.__class__.__name__}(title={self.title}, date={self.date}, \ authors={self.authors}, price={self.price}, num={self.num}, total={self.total})'
Код выше определяет тип данных Book
, для хранения информации о книге в книжном магазине. Но такая запись пользовательского типа данных очень многословна, что совсем не по питонически. Для упрощенного и удобного создания пользовательских типов данных, с версии Python 3.7 введен встроенный модуль dataclasses
. Этот модуль использует декоратор @dataclass
и несколько вспомогательных функций для упрощенного написания пользовательских типов данных.
Посмотрим, на сколько короче можно записать тип данных Book
, представленный выше:
from dataclasses import dataclass, field @dataclass class Book(): """Тип данных книга""" title: str = None date: str = None authors: list = field(default_factory=list) price: float = 0 num: int = 0 total: float = field(init=False) def __post_init__(self): # инициализация переменной `total` self.total = self.price * self.num
И это все! Коротко? На самом деле, модуль класса данных dataclasses
автоматически добавит сгенерированные специальные методы __init__()
и __repr__()
. Этот модуль дает очень много дополнительных возможностей и полезных фич при создании собственных типов данных, а вызов функции dataclasses.field()
для определенного поля, поможет его кастомизировать под свои нужды. При создании своих типов данных, обязательно используете этот модуль, т.к. он защитит вас от многих распространенных ошибок (например объявления изменчивых типов данных по умолчанию).
Примечание. модуль dataclasses
не проверяет, указанный в аннотации тип переменной!
Вернемся к пользовательским типам. И так, объявленный тип содержит шесть переменных:
title
- название книги,date
- дата выхода книги,authors
- авторы книги.price
- стоимость одной книги,num
- количество книг в магазине,total
- общая стоимость книг.Эти переменные называются полями типа данных. Код класса Book
- это простое объявление типа. Чтобы использовать тип данных Book()
, необходимо просто создать его экземпляр:
>>> book = Book()
Инициализация пользовательского типа путем присваивания значений каждому члену по порядку - занятие неблагодарное и довольно громоздкое (особенно, если много полей), поэтому легче инициализировать тип сразу, при создании экземпляра класса данных. Это позволяет инициализировать некоторые или сразу все поля объявленного типа:
# создаем отдельную переменную типа Book, например для книги gold_key >>> gold_key = Book('Золотой ключик', '1989', ['А. Толстой'], 512, 7) >>> gold_key # Book(title='Золотой ключик', date='1989', authors=['А. Толстой'], # price=512, num=7, total=3584) # создаем отдельную переменную типа Book для day_watch >>> day_watch = Book('Дневной Дозор', '2006', ['В. Васильев', 'С. Лукьяненко'], 1024, 9) >>> day_watch # Book(title='Дневной Дозор', date='2006', authors=['В. Васильев', 'С. Лукьяненко'], # price=1024, num=9, total=9216)
Для того, чтобы получить доступ к отдельным полям типа данных, используется точечная нотация. Например, в коде, приведенном ниже, используется точка для выбора значения каждого поля пользовательского типа данных:
>>> gold_key.title # 'Золотой ключик' >>> gold_key.price # 512 >>> gold_key.authors # ['А. Толстой'] >>> day_watch.title # 'Дневной Дозор' >>> day_watch.date # '2006' >>> day_watch.authors[0] # 'В. Васильев' >>> day_watch.authors # ['В. Васильев', 'С. Лукьяненко']
В этом примере, легко определить, какая переменная относится к типу gold_key
, а какая к day_watch
. Это обеспечивает гораздо более высокий уровень организации, чем в случае с обычными отдельными переменными.
Переменные - поля типа данных работают так же, как и простые переменные, поэтому с ними можно выполнять обычные операции доступные типу данных, к которому они относятся:
# присвоение новых значений >>> gold_key.title = 'Современный золотой ключик' >>> gold_key.title # 'Современный золотой ключик' >>> gold_key.authors.append('Неизвестный соавтор') >>> gold_key.authors # ['А. Толстой', 'Неизвестный соавтор'] >>> gold_key.date = 2021 >>> gold_key # Book(title='Золотой ключик', date=2021, authors=['А. Толстой', 'Неизвестный соавтор'], # price=512, num=7, total=3584) # операции сравнения отдельных полей >>> gold_key.total < day_watch.total # True
Модуль класса данных dataclasses
добавляет возможность сравнения самих типов данных с использованием хэша на основе их местоположения в памяти, как два обычных объекта.
Большим преимуществом использования пользовательских типов данных является возможность передать сразу весь тип в функцию, а не по одной переменной:
from dataclasses import dataclass, field @dataclass class Book(): """Тип данных книга""" title: str = None date: str = None authors: list = field(default_factory=list) price: float = 0 num: int = 0 total: float = field(init=False) def __post_init__(self): # инициализация переменной `total` self.total = self.price * self.num def print_state_book(book): """Печать сведений о книге""" print(f'Название: {book.title}') print('Авторы:') for author in book.authors: print(' '*2, author) print(f'Стоимость книги: {book.price:0.2f}') print(f'Количество: {book.num}') print(f'Итого: {book.total:0.2f}') >>> gold_key = Book('Золотой ключик', '1989', ['А. Толстой'], 512, 7) >>> day_watch = Book('Дневной Дозор', '2006', ['В. Васильев', 'С. Лукьяненко'], 1024, 9) >>> print_state_book(gold_key) # Название: Золотой ключик # Авторы: # А. Толстой # Стоимость книги: 512.00 # Количество: 7 # Итого: 3584.00 >>> print_state_book(day_watch) # Название: Дневной Дозор # Авторы: # В. Васильев # С. Лукьяненко # Стоимость книги: 1024.00 # Количество: 9 # Итого: 9216.00
Одни пользовательские типы данных могут создаваться на основе других. Такое поведение хорошо прослеживается на примере с точкой на плоскости и координатами прямоугольника.
from dataclasses import dataclass, field @dataclass class Point: """Точка на плоскости""" x: int = 0 y: int = 0 @dataclass class Rect: """Прямоугольник на плоскости""" top_left: Point = field(default=Point) bottom_right: Point = field(default=Point) # создаем точки на плоскости >>> point1 = Point(10, 20) >>> point2 = Point(30, 55) # чертим прямоугольник >>> rect = Rect(point1, point2) >>> rect Rect(top_left=Point(x=10, y=20), bottom_right=Point(x=30, y=55))
Пользовательские типы данных, так же можно наследовать. Например из точки на плоскости можно создать точку в пространстве, а потом нарисовать прямоугольный параллелепипед.
Примечание: мы не сильны в стереометрии и возможно нужны дополнительные данные для типа RectBox
. Этот класс создан чисто в учебных целях, что бы в совокупности проследить наследование пользовательских типов, а также создание одного типа на основе другого.
from dataclasses import dataclass, field @dataclass class Point: """Точка на плоскости""" x: int = 0 y: int = 0 # наследуемся от пользовательского типа `Point` @dataclass class SpacePoint(Point): """Точка в пространстве""" z: int = 0 # создаем тип `RectBox` на # основе типа `SpacePoint` @dataclass class RectBox: """Прямоугольный параллелепипед""" front_top_left: SpacePoint = field(default=SpacePoint) back_bottom_right: SpacePoint = field(default=SpacePoint) # создаем точки в пространстве >>> space_point1 = SpacePoint(10, 20, 10) >>> space_point2 = SpacePoint(30, 55, 80) # рисуем прямоугольный параллелепипед >>> rect_box = RectBox(space_point1, space_point2) >>> rect_box # RectBox(front_top_left=SpacePoint(x=10, y=20, z=10), # back_bottom_right=SpacePoint(x=30, y=55, z=80)) # изменим одну координату >>> rect_box.front_top_left.y = 10 >>> rect_box # RectBox(front_top_left=SpacePoint(x=10, y=10, z=10), # back_bottom_right=SpacePoint(x=30, y=55, z=80))