Введение в Python Dataclasses — Часть 2
Это вторая статья из серии статей «Введение в Python Dataclasses«. В первой части я рассказал о базовом использование dataclasses. В этой статье я расскажу о еще одной функции класса данных dataclasses.field
.
Мы видели, что классы данных генерируют свой собственный метод __init__. Где каждому определенному полю присваивается значение, переданное во время инициализации. В этот момент задаются:
- имя переменной
- тип данных
Это делает нас весьма ограниченными в использовании полей данных. Давайте обсудим некоторые ограничения, и как они могут быть решены с помощью dataclasses.field.
Комплексная инициализация
Рассмотрим сценарий, в котором вы хотите, чтобы атрибут был задан списком при инициализации. Как нам это сделать сделаем? Для этого мы можем использовать метод __post_init__.
import random from typing import List def get_random_marks(): return [random.randint(1,10) for _ in range(5)] @dataclass class Student: marks: List[int] def __post_init__(self): self.marks = get_random_marks() #Assign random speeds >>> a = Student() >>> a.marks >>> [1,4,2,6,9]
Класса данных Student ожидает список marks (оценок). Мы решили не передавать значение marks, а инициализировать его с помощью метода __post_init__. Это был единственный атрибут, который мы определили. Более того, нам пришлось вызвать get_random_marks в __post_init__.
К счастью, в Python есть более элегантное решение для нас. Можно настроить поведение полей класса данных и их влияние на класс данных с помощью dataclasses.field.
Продолжая описанный выше пример, давайте исключим необходимость вызова get_random_marks из __post_init__. Вот как мы могли бы сделать это с помощью dataclasses.field:
from dataclasses import field @dataclass class Student: marks: List[int] = field(default_factory = get_random_marks) >>> s = Student() >>> s.marks >>> [1,4,2,6,9]
Поле dataclasses.field принимает аргумент default_factory, который можно использовать для инициализации поля, если значение не было передано во время создания объекта.
default_factory должен быть вызываемым (обычно это функция), которая не принимает аргументов.
Таким образом, мы можем инициализировать поля в более сложной форме. Теперь рассмотрим еще один вариант использования.
Все поля для сравнения данных
Из прошлой статьи мы знаем, что класс данных может автоматически генерировать методы сравнения для <, =,>, <=,> =. Но особенность с ними заключалась в том, что по умолчанию все поля классов используются для сравнения, что не всегда полезно. Чаще всего это будет создавать проблемы в удобстве использования классов данных.
Рассмотрим вариант использования, в котором у вас есть класс данных для хранения информации о пользователях вашего сервиса. Пусть класс включает такие поля, как:
- Name
- Age
- Height
- Weight
И вы хотите, чтобы пользовательские объекты сравнивались только по возрасту (Age), росту (Height) и весу (Weight). Допустим вам не нужно, чтобы имя (Name) использовалось для сравнения. Это очень распространенный вариант использования для разработчиков бэкэнда.
@dataclass(order = True) class User: name: str age: int height: float weight: float
Автоматически сгенерированные методы сравнения будут сравнивать следующий кортеж:
(self.name, self.age, self.height, self.weight)
Это создает проблему. Так как мы не хотим, чтобы имя использовалось для сравнения. Итак, как нам это сделать с помощью dataclasses.field?
Вот как:
@dataclass(order = True) class User: name:str = field(compare = False) # compare = False говорит классу данных не использовать имя для методов сравнения age: int weight: float height: float >>> user_1 = User("John Doe", 23, 70, 1.70) >>> user_2 = User("Adam", 24, 65, 1.60) >>> user_1 < user_2 >>> True
По умолчанию все поля используются для сравнения, поэтому нам нужно решить, какие поля нам не нужны для сравнения, и явно определить их как field(compare=False).
Можно также рассмотреть более простой вариант использования. Давайте определим класс данных, который содержит число и его строковое представление. И мы хотим, чтобы сравнение проводилось только по значению числа, а не по его строковому представлению.
@dataclass(order = True) class Number: string: str val: int >>> a = Number("one",1) >>> b = Number("eight", 8) >>> b > a # Compares as ("eight",8) > ("one",1) >>> False #Теперь мы будем сравнивать только с помощью Number.val @dataclass(order = True) class Number: string: str: = field(compare = False) #Не используйте Number.string для сравнения val: int >>> a = Number("one", 1) >>> b = Number("eight", 8) >>> b > a # Compares (8,) > (1,) >>> True
Все поля, используемые для представления
Автоматически сгенерированный метод __repr__ использует все поля для представления. Ну, это не идеальная ситуация во многих случаях. Особенно, когда в вашем классе данных много полей. Представление одного объекта может получится довольно огромным, и это будет создавать проблемы при отладки.
@dataclass(order = True) class User: name: str = field(compare = False) age: int height: float weight: float city: str = field(compare = False) country: str = field(compare = False) >>> a = User("John Doe", 24, 1.7, 70, "Massachusetts" ,"United States of America") >>> a >>> User(name='John Doe', age=24, height=1.7, weight=70, city='Massachusetts', country='United States of America')
Представьте, что вы видите это представление в своих журналах и каждый раз набираете регулярное выражение для его поиска. Ужасно, верно?
Ну, мы можем изменить это поведение. Для варианта использования, подобного этому, вероятно, единственный полезный атрибут для представления — это name (имя). Итак, давайте просто использовать эго для __repr__:
@dataclass(order=True) class User: name: str = field(compare=False) age: int = field(repr=False) # Это говорит классу данных не показывать age в представлении height:float = field(repr=False) weight:float = field(repr=False) city:str = field(repr=False, compare=False) country:str = field(repr=False, compare=False) >>> a = User("John Doe", 24, 1.7, 70, "Massachusetts", "United States of America") >>> b = User("Adam", 24, 1.6, 65, "San Jose", "United States of America") >>> a >>> User(name='John Doe') >>> b >>> User(name='Adam') >>> b > a #Compares (24, 1.7, 70) > (23, 1.6, 65) >>> True
Это выглядит намного лучше. Простая отладка и содержательное сравнение!
Пропуск полей из инициализации
Все примеры, которые мы видели до сих пор, имели одну общую черту: мы передавали значения для объявленных полей, за исключением случаев, когда они имели значения по умолчанию. В этом случае мы можем передать значение для этого поля что бы изменить значение по умолчанию.
@dataclass class Number: string: str val:int = 0 >>> a = Number("Zero") #Не передаем значения поля по умолчанию >>> a >>> Number(string='Zero', val=0) >>> b = Number("One", 1) #Передача значения по умолчанию для поля, в котором объявлено значение по умолчанию >>> b >>> Number(string='One', val=1)
Но есть и другой случай: когда мы не можем устанавливать значение поля во время инициализации. Это еще один распространенный случай использования. Возможно, вы отслеживаете состояние объекта и всегда устанавливаете его в False во время инициализации, более того, это значение никогда не передается во время инициализации.
class User: def __init__(self, email = None): self.email = email self.verified = False #Это поле устанавливается во время инициализации, но его значение не может быть установлено вручную при создании объекта
Итак, как нам такого добиться? Вот как:
@dataclass class User: email: str = field(repr = True) verified: bool = field(repr = False, init = False, default = False) # Удаляем verified из представления, а также из __init__ >>> a = User("a@test.com") >>> a >>> User(email='a@test.com') >>> a.verified >>> False >>> b = User("b@test.com", True) # Давайте попробуем передать значение для verified >>> Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: __init__() takes 2 positional arguments but 3 were given view raw
И вуаля! Теперь мы знаем больше о гибкости в использовании классов данных.
Заключение
Надеемся, что эти два поста помогли вам получить общее представление о dataclass, и вы с нетерпением ждете возможности использовать их в своих проектах в ближайшее время!
Спасибо за чтение. Следуй за мной по Shikhar Chauhan Github, Twitter, LinkedIn.
Оригинал: Understanding Python Dataclasses — Part 2