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

Глубже в обработки данных

Обычно примеры, использующие модуль DataClasses в Python, довольно просты в использовании его функций … Теги с питоном, промежуточным, обработчиком данных, Codequality.

На DataClasses (серия 2 частей)

Обычно примеры с использованием DataClasses Модуль в Python довольно прост в использовании своих функций. Это само по себе совершенно нормально, но иногда реализация может быть очень утомительной и громоздкой. Однако DataClasses Модуль предлагает способы быть умнее, о которых редко говорят. С этой статьей я хочу это изменить. Таким образом, эта статья не охватывает темы, как когда и как их использовать. В Интернете много материала, чтобы узнать об этом.

Следующий код представляет 3D-точку, которая может быть добавлена в другую точку и умноженную на число элементов. (Примечание: это делает этот объект более похожим на вектор, на мой взгляд). Дополнительной функцией является то, что он также поддерживает итерацию и распаковку через __iter__ Метод, составление точки и число коммутативный Анкет

# Baseline solution
from dataclasses import astuple, dataclass


@dataclass
class Point:
    x: float
    y: float
    z: float

    def __add__(self, other):
        x1, y1, z1 = self
        x2, y2, z2 = other
        return Point(x1+x2, y1+y2, z1+z2)

    def __sub__(self, other):
        x1, y1, z1 = self
        x2, y2, z2 = other
        return Point(x1-x2, y1-y2, z1-z2)

    def __mul__(self, scalar):
        x, y, z = self
        return Point(scalar*x, scalar*y, scalar*z)

    def __rmul__(self, scalar):
        return self.__mul__(scalar)

    def __iter__(self):
        return iter(astuple(self))   

Это хорошая реализация, но даже при использовании того факта, что эта точка может быть распакована, она довольно утомительно и повторяется. Набор x1, y1, Для каждого метода меньше, чем идеален. Кроме того, что, если мы хотим также иметь очки в 2D, 4D или 6D? Ну, это просто, но склонно к ошибкам. Мы должны добавить/удалить определения атрибутов/полей и все ссылки на них в соответствующих методах. В этом конкретном случае это будет модифицировано шесть строк. Первая часть – легкая и вторая (очень) раздражающая. Мы могли бы сделать немного лучше и нужно заботиться только о первой части, если хотим обратиться к другим измерениям.

Использование DataClasses Interoscection

Давайте будем немного умнее и используем больше инструментов, доступных в DataClasses модуль. В частности, функция Fields () которые обнажают поля, определенные в DataClass Анкет Таким образом, вместо того, чтобы назвать каждую координату точки в различных методах работы, мы можем итерации над ними.

# Introspection based solution
from dataclasses import astuple, dataclass, fields


@dataclass
class Point:
    x: float
    y: float
    z: float

    def __add__(self, other):
        return Point(*(getattr(self, dim.name)+getattr(other, dim.name) for dim in fields(self)))

    def __sub__(self, other):
        return Point(*(getattr(self, dim.name)-getattr(other, dim.name) for dim in fields(self)))

    def __mul__(self, other):
        return Point(*(getattr(self, dim.name)*other for dim in fields(self)))

    def __rmul__(self, other):
        return self.__mul__(other)

    def __iter__(self):
        return iter(astuple(self))

Чтобы понять, как это работает, я сосредоточусь на __add__ метод Есть довольно много, чтобы распаковать. В основе – призыв к Fields () функция, которая возвращает кортеж из 3 поле Объекты, а поле объект, как это DataClass представляет атрибут. Вы можете (и должны) пойти и проверить это в реплике, это можно сделать в самом классе или на его экземпляре. Поскольку у нас есть кортеж, мы можем обратиться к нему и получить доступ к имени каждого атрибута.

>>> for field in fields(Point):
...     print(field.name)
...
x
y
z

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

>>> p = Point(1,2,3)
>>>list(getattr(p, field.name) for field in fields(p))
[1,2,3]

Теперь мы можем выполнить операцию между двумя точками, которые эксплуатируются, и распаковываем выражение генератора в аргументы Точка инициализация

Point(*(getattr(self, dim.name)+getattr(other, dim.name) for dim in fields(self))

Последний шаг – поставить это как возвратное значение __add__ Метод, а затем для реализации той же стратегии для других методов. При этом мы достигли первого шага в удалении раздражения необходимости прикоснуться к каждому методу, если изменим количество измерений точечного класса. В качестве бонуса мы также должны немного сократить общее количество строк кода, связанных с кодом.

Альтернативное решение

Я должен согласиться с тем, что использование этого уровня самоанализации DataClass может быть немного громоздким, особенно благодаря использованию getattr функция Это было бы решением, если бы мы не реализовали __iter__ метод Но так как мы поддерживаем протокол итератора, мы можем сделать что -то, что, возможно, умнее. Таким образом, вместо того, чтобы итерация над определенными полями, мы можем итерации над самого объекта!

# Iterator based solution
import operator

from dataclasses import astuple, dataclass, fields


@dataclass
class Point:
    x: float
    y: float
    z: float

    def __iter__(self):
        return iter(astuple(self))

    def __add__(self, other):
        return Point(*(operator.add(*pair) for pair in zip(self,other)))

    def __sub__(self, other):
        return Point(*(operator.sub(*pair) for pair in zip(self,other)))

    def __mul__(self, other):
        return Point(*(operator.mul(*pair) for pair in zip(self,other)))

    def __rmul__(self, other):
        return self.__mul__(other)

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

Показатели качества кода

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

Давайте рассмотрим некоторые статистические данные для трех решений, представленных перед плюс решение без использования DataClasses (Классика), который не показан, но легко получить из наивного решения на основе обработки данных.

53.41 A 24 Классик
54.28 A 21 Базовый уровень
60.22 A 16 Самоанализ
100.00 A 17 Итератор

Без слишком глубокого погружения индекс обслуживания увеличивается по мере продвижения через различные реализации. Этот результат был чем -то, чего я интуитивно ожидал. Что шокирует значение 100 для решения на основе итератора. Это требует более глубокого погружения в это позже, так как кажется маловероятным, что это ошибка в Радон библиотека И прямо сейчас я слишком не знает об этой теме, чтобы иметь представление о том, почему это так.

Каким -то образом я бы ожидал более значительного шага между классическими и базовыми решениями в обработке данных. Но учитывая, что методы, которые мы реализуем, одинаковы, и те, которые нам не нужно было писать ( __init__ , __eq__ , __repr__ ) довольно просты. Понятно, что они не надевают ” т сильно отличается.

Спектакль

Используя %timeit На моем ноутбуке я запустил быстрый эталон добавления двух очков

>>> p1 = Point3D(1,2,3)
>>> p2 = Point3D(4,5,6)
>>> %timeit p1+p2

Для трех решений, реализованных в этой статье, со следующими результатами

5,18 мкс Базовый уровень 33.00
450 нс Самоанализ 6.04
3,34 мкс Итератор 30.40

Можно сказать, что решение, основанное на самоанализе, значительно более эффективно, чем два других. Было бы интересно лучше понять, откуда берется победа (проигрывает) для решения самоанализа (итератор).

В любом случае, этот пример показывает что -то интересное, мы можем увеличить индекс обслуживания, а также повысить производительность. Этот факт не обязательно является данным, так как обычно производительность достигается за счет читаемости.

А как насчет ND -очков?

Но ситуация все еще может быть улучшена. Допустим, вы хотите иметь возможность прожить 2D и 3D -точки. Мы могли бы скопировать и вставить все определение, дать каждому классу другое имя и обязательно иметь правильное количество атрибутов. Но это было бы чрезвычайно глупо, и мы могли бы (и должны) использовать наследование (см. Предыдущий пост , который исследует абстрактные базовые классы и обработки данных) и повторно используйте весь код для операций, так как мы только что сделали их независимыми от измерения Точка живет в. Поскольку мы исследуем DataClasses , Я вернусь к первому решению.

from abc import ABC

from dataclasses import astuple, dataclass, fields


@dataclass
class BasePoint(ABC):

    def __add__(self, other):
        return self.__class__(*(getattr(self, dim.name)+getattr(other, dim.name) for dim in fields(self)))

    def __sub__(self, other):
        return self.__class__(*(getattr(self, dim.name)-getattr(other, dim.name) for dim in fields(self)))

    def __mul__(self, other):
        return self.__class__(*(getattr(self, dim.name)*other for dim in fields(self)))

    def __rmul__(self, other):
        return self.__mul__(other)

    def __iter__(self):
        return iter(astuple(self))


@dataclass
class Point2D(BasePoint):
    x: float
    y: float


@dataclass
class Point3D(BasePoint):
    x: float
    y: float
    z: float

Превосходно Теперь у нас есть точки в любом измерении, которое мы хотим без особых усилий!

Фабрика очков

Но есть ли способ сделать еще меньше работы, чем это? DataClasses Модуль имеет изящную функцию, называемую make_dataclass что, как говорится в его названии, делает обработки данных на основе своих аргументов.

Мы можем попытаться создать точку в 1D

>>> Point1D = make_dataclass('Point1D', [('x',float)], bases=(BasePoint,))
>>> Point1D(1)
Point1D(x=1)

По сравнению с определением класса нормальным образом, это не кажется большой победой. Но что, если мы хотим создать экзотическую точку в 5 измерениях? Ну, сначала мы создаем список кортежей с именами поля и типами, а затем используем make_dataclass с этим.

>>> dims = 5
>>> fields_definition = ((f'x{i}', float) for i in range(dims))
>>> Point5D = make_dataclass('Point5D', fields_definition, bases=(BasePoint,))
>>> Point5D(*range(5))
Point5D(x0=0, x1=1, x2=2, x3=3, x4=4)

Я перехожу от именования xyz к x {i} в более «математической» обозначении, которая для компьютеров также работает намного лучше.

Это создает сцену для создания целой семьи очков. Для этого мы создаем функцию, которая создаст их (фабрика)

def PointFactory(dim):
     fields_definition = ((f'x{i}', float) for i in range(dim))
     return make_dataclass(f'Point{dims}D', fields_definition, bases=(BasePoint,))

Использование этой фабрики серия классов, представляющих точки в разных измерениях можно легко создать

>>> point_classes = [PointFactory(dim) for dim in range(5)]
>>> point_classes
[, , , , ]
>>> point_classes[3](1,2,3)
Point3D(x0=1, x1=2, x2=3)

Вывод

Помимо сокращения шаблона, которое DataClasses Модуль предоставляет, он предлагает несколько мощных инструментов для работы с ними. В качестве примера я показал, как создать фабрику N-мерных точек.

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

После просмотра производительности каждой реализации возникает вопрос, если на основе итератора может быть улучшено, будучи умнее. По крайней мере, моя первая исследовательская попытка, переходя от оператор импорта к Из импорта оператора Add, Mul, sub не показал никаких изменений. Возможно, это было бы хорошим упражнением для читателя;)

Благодарности

Я хочу поблагодарить Nour Faroua за ее вклад, ведущий к упрощению в Кодексе, и Брайану Рейнаерту за его тщательный обзор и улучшение вкладки, объяснения и язык статьи.

На DataClasses (серия 2 частей)

Оригинал: “https://dev.to/ivergara/deeper-into-dataclasses-6fd”