Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Python

Как реализовать собственную структуру данных в Python

by
Difficulty:IntermediateLength:MediumLanguages:

Russian (Pусский) translation by Ellen Nelson (you can also view the original English article)

Python обеспечивает полноценную поддержку для реализации собственной структуры данных с использованием классов и пользовательских операторов. В этом учебном пособии вы создадите собственную структуру данных конвейера, которая может выполнять произвольные операции над данными. Мы будем использовать Python 3.

Структура данных конвейера

Структура данных конвейер интересна тем, что она очень гибкая. Он состоит из списка произвольных функций, которые могут быть применены к набору объектов и составить список результатов. Я буду использовать преимущества расширяемости Python и использовать символ вертикальной черты «pipe» («|») для построения конвейера.

Живой пример

Прежде чем погрузиться во все детали, давайте посмотрим очень простой пример в действии:

Что здесь происходит? Давайте разберём это шаг за шагом. Первый элемент range(5) создает список целых чисел [0, 1, 2, 3, 4]. Целые числа передаются в пустой конвейер, обозначенный символично Pipeline(). Затем в конвейер добавляется функция double, и, наконец, клёвая функция Ω завершает работу конвейера и приводит к тому, что он оценивает себя.

Оценка состоит в том, что берётся ввод и применяются все функции в конвейере (в данном случае просто функция double). Наконец, мы сохраняем результат в переменной с названием x и распечатываем её.

Классы Python

Python поддерживает классы и имеет очень сложную объектно-ориентированную модель, включая множественное наследование, микшины и динамическую перегрузку. Функция __init__() служит в качестве конструктора, который создает новые экземпляры. Python также поддерживает расширенную модель метапрограмминга, о которой мы не будем говорить в этой статье.

Вот простой класс, который имеет конструктор __init__(), который принимает необязательный аргумент x (по умолчанию 5) и сохраняет его в атрибуте self.x. Он также имеет метод foo(), который возвращает атрибут self.x, умноженный на 3:

Вот как создать экземпляр с явным аргументом x и без него:

Пользовательские операторы

В Python вы можете использовать пользовательские операторы для ваших классов для более красивого синтаксиса. Существуют специальные методы, известные как методы «dunder». «Dunder» означает «двойное подчеркивание». Такие методы, как «__eq__», «__gt__» и «__or__», позволяют использовать такие операторы, как «==», «>» и «|» в ваших экземплярах классов (объектов). Посмотрим, как они работают на примере класса A.

Если вы попытаетесь сравнить два разных экземпляра A друг с другом, результат всегда будет False независимо от значения x:

Это связано с тем, что Python по умолчанию сравнивает адреса памяти объектов. Предположим, мы хотим сравнить значение x. Мы можем добавить специальный оператор «__eq__», который принимает два аргумента: «self» и «other» и сравнивает их атрибут x:

Давайте проверим:

Реализация Pipeline как класса Python

Теперь, когда мы рассмотрели основы классов и пользовательских операторов в Python, давайте использовать их для реализации нашего конвейера (pipeline). Конструктор __init__() принимает три аргумента: functions, input и terminals. Аргумент «functions» это одна или несколько функций. Эти функции являются этапами в конвейере, которые работают с входными данными.

Аргумент «input» это список объектов, над которыми работает конвейер. Каждый элемент input будет обрабатываться всеми функциями конвейера. Аргумент «terminal» это список функций и когда встречается одна из них, конвейер пропускает их через себя и возвращает результат. Terminals по умолчанию используют только функцию печати (в Python 3, «print» является функцией).

Обратите внимание, что внутри конструктора, к terminals добавляется таинственная «Ω». Я объясню это позже.

Конструктор конвейера

Вот определение класса и конструктора __init__():

Python 3 полностью поддерживает Unicode в названиях идентификаторов. Это означает, что мы можем использовать классные символы типа «Ω» для названий переменных и функций. Здесь я объявил функцию тождества, называемую «Ω», которая служит в качестве терминальной функции: Ω = lambda x: x

Я мог бы использовать и традиционный синтаксис:

Операторы «__or__» и «__ror__»

Здесь идет ядро класса Pipeline. Чтобы использовать "|" (символ вертикальной черты), нам нужно переопределить пару операторов. Символ «|» используется Python для битовых или целых чисел. В нашем случае мы хотим переопределить это для реализации цепочки функций, а также для подачи ввода в начале конвейера. Это две отдельные операции.

Оператор «__ror__» вызывается, когда второй операнд является экземпляром конвейера Pipeline, а первый не является. Он рассматривает первый операнд как входной и сохраняет его в атрибуте self.input и возвращает экземпляр Pipeline обратно (себе). Это позволяет позже связывать больше функций.

Вот пример, где будет вызываться оператор __ror__(): 'hello there' | Pipeline()

Оператор «__or__» вызывается, когда первый операнд является конвейером (даже если второй операнд также является конвейером). Он принимает операнд как вызываемую функцию и это доказывает, что операнд «func» действительно является вызываемым.

Затем он добавляет функцию в атрибут self.functions и проверяет, является ли функция одной из функций terminal.  Если это terminal, весь конвейер просчитывается и возвращается результат. Если это не terminal, возвращается сам конвейер.

Оценка Pipeline

При добавлении в конвейер больше и больше не-терминальных функций, ничего не происходит. Фактическая оценка отложена до тех пор, пока не будет вызван метод eval(). Это может произойти либо путём добавления терминальной функции в конвейер, либо путём прямого вызова eval().

Оценка состоит из итерации по всем функциям в конвейере (включая терминальную функцию, если она есть) и их выполнение по порядку на выходе предыдущей функции. Первая функция в конвейере получает элемент ввода.

Эффективное использование конвейера

Один из лучших способов использования конвейера – это применение его к нескольким наборам ввода. В следующем примере определяется конвейер без входов и терминальных функций. Он имеет две функции: печально известную функцию double, которую мы определили ранее, и стандартную math.floor.

Затем мы предоставляем три разных ввода. Во внутреннем цикле мы добавляем терминальную функцию Ω, когда мы вызываем её для сбора результатов перед их выводом:

Вы можете использовать функцию терминала print напрямую, но в таком случае каждый элемент будет выведен на отдельной строке:

Дальнейшие улучшения

Вот несколько улучшений, которые могут сделать конвейер данных более полезным:

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

Вывод

Python – очень выразительный язык и хорошо оснащён для разработки собственной структуры данных и пользовательских типов. Возможность переопределять стандартные операторы очень полезна, когда семантика поддаётся такой системе обозначений. Например, символ вертикальной черты («|») («pipe») является очень естественным для конвейера («pipeline»).

Многие разработчики Python пользуются встроенными структурами данных Python, такими как кортежи, списки и словари. Однако разработка и внедрение собственной структуры данных может упростить вашу систему и облегчить работу с ней, повышая уровень абстракции и скрывая внутренние детали от пользователей. Попробуйте.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.