Получите полное представление об основах классов данных в Python
Если вам нравится работать с Medium, поддержите меня и тысячи других авторов, подписавшись на членство. Это стоит всего 5 долларов в месяц, это очень поддерживает нас, писателей, и вы получаете доступ ко всем удивительным историям на Medium.
Что такое классы данных?
Классы данных — это новая функция, доступная во встроенных модулях Python, начиная с версии 3.7. Они предоставляют декоратор и функции для создания более простых, удобных и безопасных классов, которые в основном используются для обработки данных, отсюда и название.
Одним из основных преимуществ классов данных является то, что они автоматически генерируют для вас несколько специальных методов, таких как __init__
, __repr__
и __eq__
. Это может сэкономить вам много времени и лишнего кода при определении классов, которые в основном используются для обработки данных.
Еще одним важным преимуществом классов данных является использование строгой типизации, которая гарантирует, что атрибуты экземпляра определены. Это достигается за счет использования аннотаций типов, которые позволяют указывать тип каждого атрибута при определении класса. Это может предотвратить ошибки из-за неоднозначности типов, а также упрощает отлов кода для других разработчиков.
Как использовать классы данных
Чтобы использовать классы данных, вам сначала нужно импортировать декоратор класса данных из модуля dataclasses
. Этот декоратор изначально включен в Python 3.7 и выше.
Использование классов данных очень просто. Просто украсьте определение класса декоратором @dataclass
, чтобы определить класс данных.
Вот пример простого класса данных с параметрами по умолчанию:
from dataclasses import dataclass, field @dataclass class Point: x: float y: float p = Point(1.0, 2.0) print(p) # Output: Point(x=1.0, y=2.0)
Мы определили класс данных Point
с двумя полями, x
и y
, оба из которых являются числами с плавающей запятой. Когда вы создаете экземпляр класса Point
, вы можете указать значения для x
и y
в качестве аргументов конструктора.
По умолчанию классы данных сгенерируют для вас метод __init__
, который принимает поля класса в качестве аргументов. Они также сгенерируют метод __repr__
, возвращающий строковое представление объекта, которое выводится при вызове print(p)
в приведенном выше примере.
Вы можете указать, какие методы генерируются декоратором, передав дополнительные аргументы, такие как repr=False, чтобы деактивировать метод __repr__
.
Функция поля
Вы также можете указать значения по умолчанию для полей класса данных, используя аргументы default
и default_factory
для функции field()
.
Например:
@dataclass class Point: x: float = 0.0 y: float = field(default=0.0) p1 = Point(x=1.0, y=2.0) print(p1) # Output: Point(x=1.0, y=2.0) p2 = Point(1.0) print(p2) # Output: Point(x=1.0, y=0.0)
В этом примере мы определили класс Point
с двумя полями, x
и y
. Поле x имеет значение по умолчанию 0,0, указанное непосредственно в определении поля, а поле y имеет значение по умолчанию 0,0, указанное с помощью функции field()
.
Когда мы создаем экземпляр класса Point
, мы можем указать значения для x
и y
в качестве аргументов конструктора. Если мы не укажем значение для y
, будет использоваться значение по умолчанию 0,0.
Внутри класса данных два способа указания значений по умолчанию эквивалентны для атрибутов immutable.
Однако функция field()
обеспечивает большую гибкость и дополнительные возможности для определения атрибутов.
Представление поля и аргументы инициализации
Действительно, аргументы __repr__
и __init__
по умолчанию для функции field()
имеют значение True. Мы можем установить для них значение False, чтобы изменить их поведение.
@dataclass class Point: x: float = field(default=0.0, repr=False) y: float = field(default=0.0, init=False) p1 = Point(3.0) print(p1) # Output: Point(y=0.0) p2 = Point(3.0, 2.0) # TypeError: Point.__init__() takes from 1 to 2 positional arguments but 3 were given
В первом примере экземпляр p1
не показывает значение x
в строковом представлении, что полезно для сокрытия временных или конфиденциальных переменных (по крайней мере, из строкового представления).
Во втором примере instancep2
не может быть создан, потому что аргумент инициализации имеет значение False, и мы пытаемся инициализировать переменную y
. Это полезно для переменных, которые должны возвращаться только методом. Давайте добавим в наш класс данных метод, который использует только y
в качестве результата вычисления в функции compute_y_with_x
:
@dataclass class Point: x: float = field(default=0.0, repr=False) y: float = field(default=0.0, init=False) def compute_y_with_x(self): self.y = self.x ** 2 p2 = Point(x=2.0) p2.compute_y_with_x() print(p2) # Output: Point(y=4.0)
Здесь мы замечаем, что p2
использовал y
в качестве неинициализированной переменной (init=False), которая является результатом преобразования нашей входной переменной x
. Нас не интересует x
после инициализации, поэтому мы удалили его строковое представление (repr=False).
поле default_factory
Вы помните, когда мы обсуждали, что использование аргумента класса по умолчанию или установка поля с аргументом по умолчанию имеет тот же эффект для неизменяемых объектов, но не для изменяемых?
Давайте рассмотрим пример, в котором мы пытаемся инициализировать атрибут для объекта mutable, такого как список. Мы также создадим метод, который позволит нам добавлять элементы в список с именем add_a_dimension
:
@dataclass class Points: coord: list = field(default=[]) def add_a_dimension(self, element): self.coord.append(element) # Output: ValueError
Этот класс невозможно создать, потому что, как указано, «использование изменяемого значения по умолчанию (например, списка) для поля ‹coord› не разрешено». Это мера безопасности, добавленная классами данных, которая очень полезна для предотвращения ошибок, с которыми мы можем столкнуться в обычном классе.
Действительно, если мы хотим определить класс данных Points
, используя обычный класс, это будет точно эквивалентно:
class Points: coord = [] def __init__(self, coord=coord): self.coord = coord def add_a_dimension(self, element): self.coord.append(element)
И у нас не будет ошибок при определении этого класса! Однако, если бы мы определили класс таким образом, используя обычный класс, мы бы увидели непреднамеренное поведение:
p1 = Points() p2 = Points() p1.coord, p2.coord # Output: ([],[]) p1.add_a_dimension(3)
Хорошо, мы создали пустой список и добавили 3 к экземпляру p1
. Как вы думаете, какие теперь будут значения p1.coord
и p2.coord
?
p1.coord, p2.coord # Output: ([3], [3])
Невероятно, на экземпляр p2
также повлияло добавление 3 к списку!
Это связано с тем, что в Python при создании экземпляра класса этот экземпляр будет использовать ту же копию атрибутов класса. Поскольку списки являются изменяемыми, и p1
, и p2
будут использовать одну и ту же копию списка coord
.
Чтобы правильно реализовать этот класс и избежать этого непреднамеренного результата, классы данных предоставляют аргумент для field()
с именем default_factory
.
Этот аргумент позволяет указать функцию, которая будет вызываться для создания нового значения по умолчанию для поля каждый раз при создании нового экземпляра класса. Это гарантирует, что каждый экземпляр класса будет иметь собственную уникальную копию поля, а не использовать один и тот же изменяемый объект.
@dataclass class Points: coord: list = field(default_factory=lambda: []) def add_a_dimension(self, element): self.coord.append(element) p1 = Points() p2 = Points() p1.coord, p2.coord # Output ([], [])
Стоит отметить, что ValueError не возникает, даже если мы определяем изменяемый список как значение по умолчанию для coord
.
Давайте проверим выходные данные после вызова метода add_a_dimension
только для одного экземпляра:
p1.add_a_dimension(3) p1.coord, p2.coord # Output ([3], [])
О, наконец-то мы получили желаемый результат! Экземпляр p2
не был изменен методом, вызванным экземпляром p1
. Именно потому, что у каждого экземпляра есть своя уникальная копия поля coord
.
Как мы видим, классы данных обеспечивают безопасность, правильно обрабатывая изменяемые и неизменяемые объекты.
Наследование
Последний момент, который следует рассмотреть, — это то, как классы данных работают с наследованием. По умолчанию классы данных не имеют метода __init__
, поэтому должен быть метод, позволяющий перезаписывать унаследованные атрибуты. Этот метод называется __post_init__
и вызывается после инициализации экземпляра.
Вот пример, иллюстрирующий эту концепцию:
@dataclass class Point: x: float = field(default=0.0) y: float = field(default=0.0) def __post_init__(self): self.x = self.x ** 2 self.y = self.y ** 2 @dataclass class ColoredPoint(Point): color: str = field(default='black') def __post_init__(self): self.color = self.color.upper()
В этом примере класс Point
имеет метод __post_init__
, который возводит в квадрат значения x
и y
. Класс ColoredPoint
наследуется от Point
, а также имеет собственный метод __post_init__
, который переводит значение атрибута color
в верхний регистр.
Давайте создадим экземпляр Point
:
p0 = Point(2.0,2.0) print(p0) # Output: Point(x=4.0, 4.0)
Из вывода мы видим, что был вызван метод __post_init__
, и оба значения x
и y
были возведены в квадрат.
Теперь давайте создадим экземпляр ColoredPoint
:
p1 = ColoredPoint(2.0, 2.0, 'red') print(p1) # Output: ColoredPoint(x=2.0, y=2.0, color='RED')
При создании экземпляра ColoredPoint
вызывается метод __post_init__
, и значение color
указывается в верхнем регистре, но значения x
и y
не возводятся в квадрат, знаете почему?
Это потому, что метод __post_init__
из Point
не был вызван в нашем методе __post_init__
из ColoredPoint
!
Чтобы вызвать метод __post_init__
базового класса, вы можете использовать функцию super()
, которая возвращает ссылку на базовый класс. Вот исправленная версия ColoredPoint
:
@dataclass class ColoredPoint(Point): color: str = field(default='red') def __post_init__(self): super().__post_init__() self.color = self.color.upper() p2 = ColoredPoint(2.0, 2.0, 'red') print(p2) # Output: ColoredPoint(x=4.0, y=4.0, color='RED')
С этим изменением метод __post_init__
класса Point
будет вызываться при создании экземпляра ColoredPoint
, а значения x
и y
будут возведены в квадрат, как и ожидалось.
Я рад, что смог помочь вам узнать о классах данных. Как вы видели, классы данных предоставляют расширенную версию обычных классов, которые более безопасны и подчеркивают строгую типизацию. Надеюсь, вы почувствуете себя более уверенно в своей способности использовать классы данных в своих будущих проектах.
Бесплатно вы можете подписаться на Medium по моей реферальной ссылке.
Или вы можете получать все мои сообщения в свой почтовый ящик.Сделайте это здесь!