Автор оригинала: Shikhar Chauhan.
Если вы читаете это, то вы уже знаете о Python 3.7 и новых функциях, которые поставляются вместе с ним. Лично меня больше всего волнуют Классы данных
. Я уже некоторое время жду, когда они прибудут.
Это сообщение из двух частей:
- Обзор функций класса данных в этом посте
- Класс данных
поля
обзор в следующем посте
Вступление
Классы данных
являются классами 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
Вот что изменилось с декоратором класса данных:
- Нет необходимости определять
__init__
и затем присваивать значенияself
,d
позаботится об этом - Мы заранее определили атрибуты членов в гораздо более удобочитаемой форме, а также намек на тип . Теперь мы сразу знаем, что
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: …
init
: По умолчанию будет сгенерирован метод__init__|/. Если передается как
False, класс не будет иметь метода
__init__//.repr
:__repr__
метод генерируется по умолчанию. Если передается какFalse
, класс не будет иметь метода__repr__|/.
eq: По умолчанию будет сгенерирован метод
__eq__. Если передано как
False, метод
__eq__не будет добавлен классом
данных, но по умолчанию будет использоваться для объекта
. __eq__.
порядок: По умолчанию
__gt__,
_ _ ge__,
_ _ lt__,
_ _ le_ _будут сгенерированы методы. Если они передаются как
False, они опущены.
Мы обсудим замороженные
через некоторое время. Аргумент unsafe_hash
заслуживает отдельного поста из-за его сложных вариантов использования.
Теперь, возвращаясь к нашему варианту использования, вот что нам нужно:
__init__
__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 .
Обновление: Сообщение для классов данных.поле
можно найти здесь .