Эффективное управление данными с помощью класса данных Python с использованием замороженных и свойств

В предыдущем посте я познакомил вас с декораторами Python. Сегодня мы более подробно рассмотрим некоторые встроенные декораторы — в частности, те, которые можно использовать для широкой темы управление данными. Под этим я подразумеваю классы, предназначенные для содержания и хранения данных (@dataclass), а также упрощение доступа к атрибутам за счет применения стиля объектно-ориентированного программирования (ООП) в Python (@property).

Классы данных

Декоратор dataclass в Python снабжает класс вспомогательными функциями для хранения данных, такими как автоматическое добавление конструктора, перегрузка оператора __eq__ и функции repr.

Давайте начнем с примера: мы разработаем простой класс, в котором будут храниться сотрудники компании. У них есть имя, зарплата, а также атрибут, описывающий количество лет, проведенных в компании.

В обычном Python вы, вероятно, реализовали бы это в следующих строках кода:

class Employee:
    def __init__(self, name: str, salary: int, years_with_company: int = 0):
        self.name = name
        self.salary = salary
        self.years_with_company = years_with_company

    def __eq__(self, other: "Employee"):
        return (self.name == other.name and self.salary == other.salary and self.years_with_company == other.years_with_company)

first_employee = Employee("First", 80000)
second_employee = Employee("First", 80000)

print(first_employee)
print(first_employee == second_employee)

Вышеприведенный код реализует класс с ранее описанными атрибутами, а также перегружает оператор равенства (__eq__): без этого проверка first_employee == second_employee не удалась бы, так как Python не знает, как осмысленно сравнивать этот конкретный класс, и возвращается к проверке, если два экземпляра являются одними и теми же объектами.

Далее отметим, что строковое представление (print(first_employee)) мало что нам дает, а вместо этого просто выводит что-то похожее на <__main__.Employee object at 0x7f402e682670>.

Теперь пусть Python выполнит нашу работу, украсив наш класс декоратором dataclass!

from dataclasses import dataclass

@dataclass
class Employee:
    name: str
    salary: int
    years_with_company: int = 0

first_employee = Employee("First", 80000)
second_employee = Employee("First", 80000)

print(first_employee)
print(first_employee == second_employee)

Использование этого декоратора привело к тому, что Python автоматически реализовал для нас конструктор, который работает идентично тому, что мы реализовали ранее: он присваивает атрибуты экземпляра в соответствии с переданными аргументами, а также может обрабатывать необязательные аргументы.

Затем в dataclass оператор равенства был перезаписан для проверки равенства всех атрибутов класса, идентично тому, что мы вручную реализовали выше — правильно возвращая true в этой проверке на равенство.

Кроме того, строковое представление экземпляра класса теперь выглядит намного лучше (т. е. __repr__ было перезаписано со смыслом), что дает: Employee(name=’First’, salary=80000, years_with_company=0).

Декоратор dataclass делает даже больше, но пока мы сосредоточимся на этих трех, вероятно, наиболее часто используемых эффектах.

Замороженные классы данных

Поскольку декораторы на самом деле являются функциями, они также могут принимать аргументы: frozen — один из возможных аргументов здесь. Это указывает на то, что dataclass нельзя модифицировать после строительства. В частности, следующее не удастся:

from dataclasses import dataclass

@dataclass(frozen=True)
class Employee:
    name: str
    salary: int
    years_with_company: int = 0

employee = Employee("First", 80000)

employee.name = "Test"

Это имеет смысл во многих сценариях. Например, dataclasses используются исключительно для представления неизменяемых наборов данных.

Характеристики

Давайте вернем приведенное выше изменение frozen и вместо этого сделаем более детальный контроль доступа, чтобы в целом реализовать обычный способ доступа к атрибутам экземпляра.

Этот пост не будет введением в ООП, но кратко: мы никогда не хотим напрямую раскрывать атрибуты экземпляра. Вместо этого мы осмысленно разделим функции/переменные на public, private или protected (возможно, я склоняюсь к C++, но на этом все) и предоставим общедоступные переменные с помощью методов получения и установки — короче говоря: свойств.

Метод получения будет отвечать за возврат значения переменной, а метод установки используется для его установки. Это помогает лучше контролировать переменные (например, мы не хотим разрешать устанавливать делитель равным 0).

На этот раз я пропущу утомительный пример реализации геттера/сеттера и вместо этого покажу решение непосредственно с помощью декоратора Python property:

from dataclasses import dataclass

@dataclass()
class Employee:
    _name: str
    _salary: int
    _years_with_company: int = 0

    @property
    def name(self):
        return self._name

    @property
    def salary(self):
        return self._salary

    @salary.setter
    def salary(self, new_salary: int):
        if new_salary > self.salary:
            self._salary = new_salary
        else:
            print("New salary must be greater than previous!")


employee = Employee("First", 80000)

employee.salary = 75000
employee.salary = 85000
print(employee)
employee.name = "this will fail"

Обратите внимание на переименование переменных. Теперь мы используем начальное подчеркивание (_): по соглашению Python. Эта переменная понимается как protected — это означает, что вы не должны обращаться к ней напрямую.

Также обратите внимание, как @property и @salary.setter плюс декорированные функции используются для определения геттера и сеттера, и что employee.salary затем может использоваться как любая другая переменная (в этом примере мы предполагаем щедрый отдел кадров, который допускает только повышение зарплаты). Далее мы пропустили реализацию сеттера для name, сделав эту переменную доступной только для чтения (опять же, не очень реалистичное предположение, хотя имена могут измениться в будущем).

Обратите внимание, что я не реализовал подобные вещи для years_with_company, но это будет выглядеть идентично.

На этом мы заканчиваем тему «управление данными». Спасибо, что настроились, и надеюсь скоро увидеть вас снова для новой статьи!