Рубрики
Без рубрики

Понимание классов данных Python — Часть 1

Python 3.7 вводит новые классы данных. В этом посте я расскажу о том, как они работают и как вы можете приспособить их к своему варианту использования.

Автор оригинала: Shikhar Chauhan.

Если вы читаете это, то вы уже знаете о Python 3.7 и новых функциях, которые поставляются вместе с ним. Лично меня больше всего волнуют Классы данных . Я уже некоторое время жду, когда они прибудут.

Это сообщение из двух частей:

  1. Обзор функций класса данных в этом посте
  2. Класс данных поля обзор в следующем посте

Вступление

Классы данных являются классами Python, но подходят для хранения объектов данных. Что такое объекты данных, спросите вы? Вот неполный список функций, определяющих объекты данных:

  • Они хранят данные и представляют определенный тип данных. Пример: Номер. Для людей, знакомых с ORMS, экземпляр модели-это объект данных. Он представляет собой определенный вид сущности. Он содержит атрибуты, которые определяют или представляют сущность.
  • Их можно сравнить с другими объектами того же типа. Например: Число может быть больше , меньше или равно другому числу

Конечно, есть и другие функции, но этого списка достаточно, чтобы помочь вам понять суть.

Чтобы понять Классы данных , мы будем реализовывать простой класс, который содержит число и позволяет нам выполнять вышеупомянутые операции. Сначала мы будем использовать обычные классы, а затем мы будем использовать Классы данных для достижения того же результата.

Но прежде чем мы начнем, несколько слов об использовании классов данных

Python 3.7 предоставляет декоратор класс данных , который используется для преобразования класса в класс данных.

Все, что вам нужно сделать, это завернуть класс в декоратор:

from dataclasses import dataclass

@dataclass
class A:
 …

Теперь давайте погрузимся в использование того, как и что класс данных изменяется для нас.

Инициализация

Обычный

class Number:

__init__ (self, val):
 self.val = val
 
>>> one = Number(1)
>>> one.val
>>> 1

С классом данных

@dataclass
class Number:
 val:int 
 
>>> one = Number(1)
>>> one.val
>>> 1

Вот что изменилось с декоратором класса данных:

  1. Нет необходимости определять __init__ и затем присваивать значения self , d позаботится об этом
  2. Мы заранее определили атрибуты членов в гораздо более удобочитаемой форме, а также намек на тип . Теперь мы сразу знаем, что val имеет тип int . Это определенно более читабельно, чем обычный способ определения членов класса.

Дзен Python: Читабельность имеет значение

Также можно определить значения по умолчанию:

@dataclass
class Number:
    val:int = 0

Представление

Представление объекта – это осмысленное строковое представление объекта, которое очень полезно при отладке.

Представление объектов python по умолчанию не очень значимо:

class Number:
    def __init__ (self, val = 0):
    self.val = val
 
>>> a = Number(1)
>>> a
>>> < __main__.Number object at 0x7ff395b2ccc0>

Это не дает нам никакого представления о полезности объекта и приведет к ужасному опыту отладки.

Содержательное представление может быть реализовано путем определения метода __repr__ в определении класса.

def __repr__ (self):
    return self.val

Теперь мы получаем осмысленное представление объекта:

>>> a = Number(1)
>>> a
>>> 1

класс данных автоматически добавляет функцию __repr__ , чтобы нам не пришлось реализовывать ее вручную.

@dataclass
class Number:
    val: int = 0

>>> a = Number(1)
>>> a
>>> Number(val = 1)

Сравнение данных

Как правило, объекты данных необходимо сравнивать друг с другом.

Сравнение двух объектов a и b обычно состоит из следующих операций:

  • a < b
  • a > b
  • a
  • a
  • a

В python можно определить [методы](https://docs.python.org/3/reference/datamodel.html#object. lt ) в классах, которые могут выполнять вышеуказанные операции. Для простоты и для того, чтобы этот пост не вышел из строя, я буду только демонстрировать реализацию == и < .

Обычный

class Number:
    def __init__ ( self, val = 0):
       self.val = val
 
    def __eq__ (self, other):
        return self.val == other.val
 
    def __lt__ (self, other):
        return self.val < other.val

С классом данных

@dataclass(order = True)
class Number:
    val: int = 0

Ага, вот и все.

Нам не нужно определять методы __eq__ и __lt__ , потому что класс данных декоратор автоматически добавляет их в определение класса для нас при вызове с помощью order

Ну и как он это делает?

Когда вы используете класс данных, он добавляет функции __eq__ и _ _ lt_ _ в определение класса. Мы уже знаем это. Итак, откуда эти функции знают, как проверить равенство и провести сравнение?

Класс данных, созданный функцией __eq__ , будет сравнивать кортеж своих атрибутов с кортежем атрибутов другого экземпляра того же класса. В нашем случае вот что будет эквивалентно автоматически сгенерированной __eq__ функции:

def __eq__ (self, other):
    return (self.val,) == (other.val,)

Давайте рассмотрим более сложный пример:

Мы напишем класс данных Person , чтобы сохранить их имя и возраст .

@dataclass(order = True)
class Person:
    name: str
    age:int = 0

Автоматически сгенерированный метод __eq__ будет эквивалентен:

def __eq__ (self, other):
    return (self.name, self.age) == ( other.name, other.age)

Обратите внимание на порядок атрибутов. Они всегда будут создаваться в том порядке, в котором вы определили их в определении класса данных.

Аналогично, эквивалентная функция __le__ была бы сродни:

def __le__ (self, other):
    return (self.name, self.age) <= (other.name, other.age)

Необходимость в определении функции типа __le__ обычно возникает, когда вам нужно отсортировать список объектов данных. Встроенная в Python функция sorted основана на сравнении двух объектов.

>>> import random

>>> a = [Number(random.randint(1,10)) for _ in range(10)] #generate list of random numbers

>>> a

>>> [Number(val=2), Number(val=7), Number(val=6), Number(val=5), Number(val=10), Number(val=9), Number(val=1), Number(val=10), Number(val=1), Number(val=7)]

>>> sorted_a = sorted(a) #Sort Numbers in ascending order

>>> [Number(val=1), Number(val=1), Number(val=2), Number(val=5), Number(val=6), Number(val=7), Number(val=7), Number(val=9), Number(val=10), Number(val=10)]

>>> reverse_sorted_a = sorted(a, reverse = True) #Sort Numbers in descending order

>>> reverse_sorted_a

>>> [Number(val=10), Number(val=10), Number(val=9), Number(val=7), Number(val=7), Number(val=6), Number(val=5), Number(val=2), Number(val=1), Number(val=1)]

класс данных как вызываемый декоратор

Не всегда желательно, чтобы были определены все методы dunder . Ваш вариант использования может состоять только из хранения значений и проверки равенства. Таким образом, вам нужны только определенные методы __init__ и _ _ eq__ . Если бы мы могли сказать декоратору, чтобы он не создавал другие методы, это уменьшило бы некоторые накладные расходы, и у нас были бы правильные операции, доступные для объекта данных.

К счастью, этого можно достичь, используя класс данных декоратор в качестве вызываемого объекта.

Из официального docs декоратор может использоваться в качестве вызываемого объекта со следующими аргументами:

[@dataclass](http://twitter.com/dataclass "Twitter profile for @dataclass")(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
 …
  1. init : По умолчанию будет сгенерирован метод __init__|/. Если передается как False , класс не будет иметь метода __init__//. repr :
  2. __repr__ метод генерируется по умолчанию. Если передается как False , класс не будет иметь метода __repr__|/. eq : По умолчанию будет сгенерирован метод __eq__
  3. . Если передано как False , метод __eq__ не будет добавлен классом данных , но по умолчанию будет использоваться для объекта . __eq__ . порядок : По умолчанию __gt__
  4. , _ _ ge__ , _ _ lt__ , _ _ le_ _ будут сгенерированы методы. Если они передаются как False , они опущены.

Мы обсудим замороженные через некоторое время. Аргумент unsafe_hash заслуживает отдельного поста из-за его сложных вариантов использования.

Теперь, возвращаясь к нашему варианту использования, вот что нам нужно:

  1. __init__
  2. __eq__

Эти функции генерируются по умолчанию, поэтому нам нужно, чтобы другие функции не создавались. Как нам это сделать? Просто передайте генератору соответствующие аргументы как ложные.

[@dataclass](http://twitter.com/dataclass "Twitter profile for @dataclass")(repr = False) # order, unsafe_hash and frozen are False
class Number:
    val: int = 0

>>> a = Number(1)

>>> a

>>> < __main__.Number object at 0x7ff395afe898>

>>> b = Number(2)

>>> c = Number(1)

>>> a == b

>>> False

>>> a < b

>>> Traceback (most recent call last):
 File "", line 1, in 
TypeError: '<' not supported between instances of 'Number' and 'Number'

Замороженные экземпляры

Замороженные экземпляры-это объекты, атрибуты которых не могут быть изменены после инициализации объекта.

Невозможно создать действительно неизменяемые объекты Python

Создание неизменяемых атрибутов для объекта в Python-трудная задача, и я не буду углубляться в этот пост.

Вот что мы ожидаем от неизменяемого объекта:

>>> a = Number(10) #Assuming Number class is immutable

>>> a.val = 10 # Raises Error

С помощью классов данных можно определить замороженный объект, используя класс данных декоратор в качестве вызываемого объекта с аргументом frozen=True .

При создании экземпляра замороженного объекта dataclass любая попытка изменить атрибуты объекта вызывает FrozenInstanceError .

@dataclass(frozen = True)
class Number:
    val: int = 0

>>> a = Number(1)

>>> a.val

>>> 1

>>> a.val = 2

>>> Traceback (most recent call last):
 File "", line 1, in 
 File "", line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'val'

Таким образом, замороженный экземпляр – отличный способ хранения

  • константы
  • настройки

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

Обработка после инициализации

С классами данных было выполнено требование определения метода __init__ для присвоения переменных self . Но теперь мы теряем гибкость выполнения вызовов функций/обработки, которые могут потребоваться сразу после назначения переменных.

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

Обычный

import math

class Float:
    def __init__ (self, val = 0):
        self.val = val
        self.process()
 
    def process(self):
        self.decimal, self.integer = math.modf(self.val)
 
>>> a = Float( 2.2)

>>> a.decimal

>>> 0.2000

>>> a.integer

>>> 2.0

К счастью, обработка после инициализации уже выполняется с помощью метода __post_init__ .

Сгенерированный метод __init__ вызывает метод __post_init__ перед возвращением. Таким образом, в этой функции может быть выполнена любая обработка.

import math

@dataclass
class FloatNumber:
    val: float = 0.0
 
    def __post_init__ (self):
        self.decimal, self.integer = math.modf(self.val)
 
>>> a = Number(2.2)

>>> a.val

>>> 2.2

>>> a.integer

>>> 2.0

>>> a.decimal

>>> 0.2

Аккуратно!

Наследование

Классы данных поддерживают наследование, как обычные классы python.

Таким образом, атрибуты, определенные в родительском классе, будут доступны в дочернем классе.

@dataclass
class Person:
    age: int = 0
    name: str

@dataclass
class Student(Person):
    grade: int

>>> s = Student(20, "John Doe", 12)

>>> s.age

>>> 20

>>> s.name

>>> "John Doe"

>>> s.grade

>>> 12

Обратите внимание на то, что аргументы Student находятся в порядке полей, определенных в определении класса.

Как насчет поведения __post_в нем__ во время наследования?

Поскольку __post_в нем__ – это просто еще одна функция, она должна быть вызвана в обычной форме:

@dataclass
class A:
    a: int
    
    def __post_init__ (self):
        print("A")

@dataclass
class B(A):
    b: int
    
    def __post_init__ (self):
        print("B")

>>> a = B(1,2)

>>> B

В приведенном выше примере вызывается только B/|/__post_в нем__ . Как мы вызываем A/|/__post_init__//?

Поскольку это функция родительского класса, ее можно вызвать с помощью super.

@dataclass
class B(A):
    b: int
    
    def __post_init__ (self):
        super(). __post_init__ () #Call post init of A
        print("B")

>>> a = B(1,2)

>>> A
    B

Вывод

Итак, выше приведено несколько способов, с помощью которых классы данных облегчают жизнь разработчикам Python.

Я старался быть тщательным и охватить большинство случаев использования, но ни один человек не идеален. Обратитесь, если вы обнаружите ошибки или захотите, чтобы я обратил внимание на соответствующие варианты использования.

Я буду охватывать классы данных.поле и unsafe_hash в разных сообщениях.

Следуйте за мной на GitHub , Twitter .

Обновление: Сообщение для классов данных.поле можно найти здесь .