Сообщить об ошибке.

Создание пользовательских типов данных в Python.

В программировании на 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))