Если вы читаете эту статью, то возможно вы уже знакомы с Python 3.7 и читали о его новых функциях. Это статья посвящена типу классов, а именно Dataclasses. Я долго ждал их появление.
Эта серия статей будет состоять из двух частей:
Dataclasses — это новые классы в Python, которые предназначены создания объектов данных (или как их еще называют классов данных). Вы спрашиваете, что такое объекты данных? Вот неполный список функций, которые определяют объекты данных:
У Dataclasses есть, конечно, больше возможностей, но этого списка достаточно для начало, чтобы помочь вам понять суть.
Чтобы разобраться с Dataclasses, мы реализуем простой класс, который будет содержат простое число и позволит нам выполнить вышеупомянутые операции. Сначала мы используем обычные классы для этого, а затем сделаем все тоже самое с помощью Dataclasses.
Но прежде чем мы начнем, пару слов о том как создать Dataclasses
В Python 3.7 предоставлен новый декоратор dataclass, который используется для преобразования обычного класса в класс данных (dataclass).
Все, что вам нужно сделать, это обернуть класс в декоратор:
from dataclasses import dataclass @dataclass class A: …
Теперь давайте углубимся в использование Dataclasses.
Обычный класс
class Number: __init__(self, val): self.val = val >>> one = Number(1) >>> one.val >>> 1
dataclass
@dataclass class Number: val:int >>> one = Number(1) >>> one.val >>> 1
Что изменилось с использованием декоратора dataclass:
Zen of Python: читабельность важна
Также возможно определить значения по умолчанию:
@dataclass class Number: val:int = 0
Представление объекта — это значимое строковое представление объекта, которое очень полезно при отладке.
Представление объектов Python по умолчанию не особо понятно и читабельно, обычно это что типа такого object at 0x7ff395b2ccc0:
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
dataclass автоматически добавляет функцию __repr__, поэтому нам не нужно ее реализовывать вручную.
@dataclass class Number: val: int = 0 >>> a = Number(1) >>> a >>> Number(val = 1)
Как правило, объекты данных необходимо сравнивать друг с другом.
Сравнение между двумя объектами a
иb
обычно состоит из следующих операций:
В python можно определить методы в классах, которые могут выполнять вышеуказанные операции. Для простоты, я продемонстрирую лишь реализацию == и <.
Обычный класс
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
@dataclass(order = True) class Number: val: int = 0
Да, вот и все.
Нам не нужно определять методы __eq__ и __lt__, потому что декоратор dataclass автоматически добавляет их в определение класса при вызове с order = True
Ну, как это реализуется?
Когда вы используете dataclass, он добавляет функции __eq__ и __lt__ в определение класса. Мы уже знаем это. Как эти функции знают, что нужно проверить равенство или сделать сравнение?
Сгенерированная функцией __eq__ будет сравнивать кортеж своих атрибутов с кортежем атрибутов другого экземпляра того же класса. В нашем случае вот что эквивалентно автоматически сгенерированной функции __eq__:
def __eq__(self, other): return (self.val,) == (other.val,)
Давайте посмотрим на более сложный пример:
Мы напишем класс данных Person, которое будет содержать имя (name) и возраст (age).
@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 основана на сравнении двух объектов.
>>> 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)]
dataclass
как вызываемый декораторНе всегда желательно, что бы были определены все методы. Возможно вам понадобится только хранение значений и проверка равенства. Таким образом, вам нужны только определенные методы __init__ и __eq__. Если бы мы могли сказать декоратору не генерировать другие методы, это уменьшило бы некоторые накладные расходы, и у нас были бы только нужные операции над объектом данных.
К счастью, этого можно достичь, используя декоратор класса данных в качестве вызываемого объекта.
Из официальных документов декоратор может использоваться как вызываемый со следующими аргументами:
@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__ не будет добавлен классом данных, но по умолчанию все равное будет object.__eq__. order
: По умолчанию генерируются методы __gt__, __ge__, __lt__, __le__. Если будет False, они не будут заданы. Аргумент frozen мы обсудим чуть позже. Аргумент unsafe_hash заслуживает отдельного поста из-за его сложных вариантов использования.
Сейчас вернемся к нашему примеру использования, вот что нам нужно:
1. __init__
2. __eq__
Эти функции генерируются по умолчанию, поэтому нам не нужно генерировать другие функции. Как нам это сделать? Просто передайте соответствующим аргументам значение False.
@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 “<stdin>”, line 1, in <module> TypeError: ‘<’ not supported between instances of ‘Number’ and ‘Number’
Frozen (Замороженные) экземпляры — это объекты, атрибуты которых нельзя изменить после инициализации объекта.
В Python невозможно создать действительно неизменяемые объекты. Всегда можно найти способ изменить объект.
Создать неизменяемые атрибуты в объекте в Python — трудная задача, и я не буду подробно останавливаться на этом.
Вот что мы ожидаем от неизменного объекта:
>>> a = Number(10) # Предполагая, что числовой класс является неизменным >>> a.val = 10 # Тут должна появится Error
С помощью классов данных можно определить замороженный объект, используя декоратор dataclass как вызываемый объект с аргументом frozen = True.
Когда создается экземпляр замороженного объекта класса данных, любая попытка изменить атрибуты объекта вызывает FrozenInstanceError.
@dataclass(frozen = True) class Number: val: int = 0 >>> a = Number(1) >>> a.val >>> 1 >>> a.val = 2 >>> Traceback (most recent call last): File “<stdin>”, line 1, in <module> File “<string>”, line 3, in __setattr__ dataclasses.FrozenInstanceError: cannot assign to field ‘val’
Таким образом, замороженный экземпляр — отличный способ для хранения…
Как правило, они не меняются в течение срока службы приложения, и любая попытка их изменения должна быть отклонена.
С помощью Dataclasses мы может отказаться от использования метода __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
Изящно, не правда ли!
Dataclasses
поддерживают наследование так же как обычные классы 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_init__ во время наследования?
Поскольку __post_init__ — это просто еще одна функция, ее вызов не меняется:
@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
В приведенном выше примере вызывается только метод __post_init__ класса B. Но как нам вызвать метод __post_init__ класса A?
Поскольку это функция родительского класса, его можно вызвать с помощью 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
Итак, выше приведены несколько способов, которыми Dataclasses облегчают жизнь разработчикам Python. Я старался быть тщательным и охватывать большинство случаев использования, но ни один человек не идеален. Пишите, если вы обнаружите ошибки или захотите, чтобы я обратил внимание на какие то другие варианты использования.
В следующей статье я расскажу о dataclasses.field.
Оригинал статьи: Shikhar Chauhan Understanding Python Dataclasses — Part 1
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…