Python – объектно-ориентированный язык, поэтому часто приходится писать классы. Реализация специальных методов дает ту или иную функциональность. Однако их написание может обернуться муторным делом. Поэтому в Python есть стандартный модуль dataclass, который упрощает разработку, автоматизируя реализацию специальных методов. Читайте в этой статье: как стандартный модуль dataclasses уменьшит объемы кода, сделать атрибуты неизменяемыми (immutable), а также как реализовать методы сравнения на Python.
Пример, когда много кода
Допустим, в вашем проекте есть класс Person
(человек). У этого класса есть такие атрибуты, как name
и id
, строкового и целочисленного типов соответственно. На начальном этапе код с вашим классом в Python будет выглядеть так
class Person: def __init__(self, id: int, name: str): self.id: int = id self.name: str = name
Однако в редких случаях нам требуется класс, у которого есть только конструктор, поэтому код начинает расти. Приходится реализовывать различные методы под все новые нужды:
- для удобного вывода реализуем метод
__repr__
; - нужно сравнить на равенство двух объектов, тогда реализуем метод
__eq__
, не забывая проверить, что сравниваемые объекты – экземпляры одного класса; - а что если не равны? тогда еще реализуем метод
__ne__
; - нужно не забыть реализовать методы сравнения
__gt__
,__lt__
, например, по идентификатору и имени; - а если кто-то захочет хранить объекты
Person
в словаре? тогда нужно реализовать еще метод__hash__
; - раз у нас атрибуты теперь хэшируемые, тогда лучше сделать их неизменяемыми (immutable), поэтому перед именами атрибутов ставим два подчеркивания:
__id
,__name
; - раз атрибуты неизменяемые, то тогда нужно реализовать геттеры и сеттеры с помощью
@property
и.setter
;
Все перечисленное – это стандартная практика написания многофункционального класса. Реализовать методы достаточно просто, но это занимает достаточно много времени и места, а также придется заняться рефакторингом, например, поменять id
на __id
во всех методах. Более того, могут появиться новые атрибуты, которые также потребует изменить свой код.
Для решения этих проблем используйте модуль dataclasses.
Python-модуль dataclasses автоматически добавляет методы
Итак, чтобы воспользоваться возможностями стандартного Python-модуля dataclasses, задекорируйте свой класс специальной функцией dataclass
, а внутри класса перечислите необходимые атрибуты (предварительно их аннотировав, т.е. расставить типы). Выглядит это следующим образом:
from dataclass import dataclass @dataclass class Person: id: int name: str
Взглянем, какие методы были реализованы после использования dataclass
. Для это мы используем стандартный модуль inspect
. У него есть функция getmember
, которая вернет всех членов объекта, удовлетворяющих условию. В нашем случае условием будет проверка ismethod
. Код на Python:
import inspect p = Person(1, "Roman") print(p) print(inspect.getmembers(p, inspect.ismethod))
Результат вывода на печать:
Person(i=1, name='Roman') [('__eq__', ... of Person(id=1, name='Roman')>), ('__init__', ...of Person(id=1, name='Roman')>), ('__repr__', ...of Person(id=1, name='Roman')>)]
Как видите, были реализованы три метода __eq__
, __repr__
, __init__
. Метод __eq__
сравнивает на равенство все атрибуты; вручную нам пришлось прописывать так:
def __eq__(self, other): if other.__class__ is self.__class__: return (self.id, self.name) == (other.id, other.name) return NotImplemented
Делаем атрибуты неизменяемыми
Чтобы сделать объект хэшируемым, его атрибуты должны быть неизменяемыми. Если вам это нужно, то передайте в функцию dataclass
аргумент frozen=True
.
@dataclass(frozen=True) class Person: id: int name: str
Если снова вывести на печать используемые методы, то получим дополнительно еще 3 новых:
Person(id=1, name='Roman') [('__delattr__', ... of Person(id=1, name='Roman')>), ('__eq__', ... of Person(id=1, name='Roman')>), ('__hash__', ... of Person(id=1, name='Roman')>), ('__init__', ... of Person(id=1, name='Roman')>), ('__repr__', ... of Person(id=1, name='Roman')>), ('__setattr__', ... of Person(id=1, name='Roman')>)]
Поскольку теперь атрибуты неизменяемые, методы __setattr__
и __delattr__
будут вызваны при попытке занести/удалить новые значения и выдадут ошибку. Поэтому мы не можем написать что-то вроде такого:
p.name = "Oleg" ## Ошибка. При frozen=True такок делать нельзя
Если вам нужна копия объекта с неизменяемыми данными, то используйте функцию replace
из модуля dataclasses. Причем, этой копии можно передать другие значения атрибутов. Например, так:
p = Person(1, "Roman") p2 = dataclasses.replace(p, id=3) # Имя будет такое же, но id - нет
Методы сравнения
Чтобы активировать все методы сравнения, передайте в функцию dataclass
аргумент order=True
. Вот так это выглядит в Python:
@dataclass(order=True) class Person: ...
Тогда все методы сравнения будут реализованы:
Person(id=1, name='Roman') [('__eq__', ... of Person(id=1, name='Roman')>), ('__ge__', ... of Person(id=1, name='Roman')>), ('__gt__', ... of Person(id=1, name='Roman')>), ('__init__', ... of Person(id=1, name='Roman')>), ('__le__', ... of Person(id=1, name='Roman')>), ('__lt__', ... of Person(id=1, name='Roman')>), ('__repr__', ... of Person(id=1, name='Roman')>)]
Значения по умолчанию
Атрибутам можно установить значения по умолчанию. Так, если в конструктор не передать значения каким-то атрибутам, то будут задействованы значения по умолчанию. С некоторыми типами данных нужно быть осторожным. В Python списки – изменяемый (mutable) объект. Поэтому если вы напишете, что-то вроде friends: List[str] = []
, то все экземпляры класса будут обращаться к одному и тому же списку. Чтобы этого избежать используется функция field
из dataclasses:
from typing import List @dataclass(order=True) class Person: id: int name: str = "Default Name" friends: List[str] = field(default_factory=list)
Как видите, мы передаем в функцию field
аргумент default_factory
со значением list
. Теперь все экземпляры класса будут иметь свой собственный список друзей, причем, по умолчанию это пустой список. Стоит не забывать, что сначала должны идти атрибуты без значений по умолчанию, затем атрибуты со значениями по умолчанию.
Заметим, что для аннотации списка используется List
из typing
. В Python 3.9 можно обойтись и без него, но в ранних версиях используйте typing
.
Функция field ограничивает использование атрибута в методах
Функция field
– многогранна, вы можете ознакомиться с ее аргументами в документацией. Некоторые из них мы рассмотрим.
Вы могли заметить, что методы сравнения, вывода на печать, хэширования используют все атрибуты. Такой расклад не всегда требуется. Чтобы атрибут не был задействован в том или ином методе, ставится флаг False
напротив соответствующего аргумента. Например:
compare=False
не будет использовать атрибут в методах сравнения;hash=False
не будет использовать атрибут в методе хэширования;repr=False
не будет выводить атрибут на печать.
Дополним код нашего класса атрибутом salary
(зарплата), которая не будет участвовать в сравнении и не будет выводиться на печать:
@dataclass(order=True) class Person: id: int salary: int = field(repr=False, compare=False) name: str = "Default Name" friends: List[str] = field(default_factory=list)
Посмотрим, что в итоге получилось. Создадим два экземпляра, в каждый из которых добавим свой список друзей. Код на Python выглядит следующим образом:
p1 = Person(id=1, salary=100) p2 = Person(id=2, salary=200) p1.friends += ["Anna", "Sergey"] p2.friends += ["Oleg", "Natasha"] print(p1) print(p2) print(inspect.getmembers(p, inspect.ismethod))
Результат вывода на печать:
Person(id=1, name='Default Name', friends=['Anna', 'Sergey']) Person(id=2, name='Default Name', friends=['Oleg', 'Natasha'])
Если вы используйте функцию field
и вам нужно передать значение по умолчанию, то передайте его в аргумент default
.
О том, как применять ООП в Python на реальных примерах Data Science вы узнаете на специализированном курсе «DPREP: Подготовка данных для Data Mining на Python» в лицензированном учебном центре обучения и повышения квалификации разработчиков, менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data в Москве.