Создаем классы за пару строчек

автор рубрика
Создаем классы за пару строчек

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 в Москве.

Комментировать