Введение в Python Dataclasses – Часть 2

Spread the love

Это вторая статья из серии статей “Введение в 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 GithubTwitterLinkedIn.

Оригинал: Understanding Python Dataclasses — Part 2


Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *