Необычный Python в обычных библиотеках

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


Специалист в Data Science из Amazon буквально прочитал код самых распространённых библиотек Python. В этом материале он делится секретами работы с Python, о которых раньше узнал из этих библиотек. За подробностями приглашаем под кат к старту нашего флагманского курса по Data Science:

  • Вызов super() в базовых классах
  • Когда использовать миксины
  • Использование относительного импорта
  • Когда добавлять код в __init__.py
  • Когда использовать экземпляр, класс и @staticmethod
  • Скрытая функция conftest.py
  • Поясняющая документация библиотек


Вызов super() в базовых классах


Функция super() в Python позволяет наследовать базовые классы (они же суперклассы или родительские классы) без необходимости явно ссылаться на базовый класс. Обычно метод super() используется в методе __init__. Множественное наследование практически невозможно без super(), хотя оно может быть удобно при одиночном наследовании.

Одно из интересных применений super() — его вызов в классе базовом классе. Этот приём я заметил в requests.adapters, в BaseAdapter:

class BaseAdapter:
    """The Base Transport Adapter"""

    def __init__(self):
        super().__init__()

    def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):

Базовый класс ни от чего не наследуется, так зачем же вызывать в нём super()?

Немного покопавшись, я узнал вот что: в базовом классе ключевое слово super() позволяет реализовать совместное множественное наследование. Без него вызовы __init__ родительских классов — после класса без super — пропускаются. Ниже — пример с базовым классом BaseEstimator и миксином ServingMixin, где DecisionTree унаследует оба класса.

Итак, у нас BaseEstimator, который в своём __init__ не вызывает super(). Для вывода атрибутов у него есть базовый метод __repr__:

class BaseEstimator:
    def __init__(self, name, **kwargs):
        self.name = name

    def __repr__(self):
        return f', '.join(f'{k}: {v}' for k, v in vars(self).items())

Затем мы наследуем BaseEstimator через подкласс DecisionTree. Всё работает нормально — при печати экземпляра DecisionTree отображаются атрибуты BaseEstimator и DecisionTree:

class DecisionTree(BaseEstimator):
    def __init__(self, depth, **kwargs):
        super().__init__(**kwargs)
        self.depth = depth

dt = DecisionTree(name='DT', depth=1)
print(dt)
> name: DT, depth: 1

Теперь пронаследуемся от ServingMixin и создадим экземпляр DecisionTree:

class ServingMixin:
    def __init__(self, mode, **kwargs):
        super().__init__(**kwargs)
        self.mode = mode

class DecisionTree(BaseEstimator, ServingMixin):
    def __init__(self, depth, **kwargs):
        super().__init__(**kwargs)
        self.depth = depth

dt = DecisionTree(name='Request Time DT', depth=1, mode='online')
print(dt)
> name: Request Time DT, depth: 1

dt.mode
> AttributeError: 'DecisionTree' object has no attribute 'mode'

Заметно, что ServingMixin не наследуется должным образом: атрибут ServingMixin (mode) не отображается, когда выводится экземпляр дерева решений, и, если мы пытаемся получить доступ к атрибуту mode, то окажется, что он не существует.

Это связано с тем, что без super() в BaseEstimator класс DecisionTree не вызывает следующий родительский класс в порядке разрешения методов.

Исправить это можно вызовом super() в BaseEstimator, и DecisionTree заработает, как ожидалось:

class BaseEstimator:
    def __init__(self, name, **kwargs):
        self.name = name
        super().__init__(**kwargs)

    def __repr__(self):
        return f', '.join(f'{k}: {v}' for k, v in vars(self).items())

class ServingMixin:
    def __init__(self, mode, **kwargs):
        super().__init__(**kwargs)
        self.mode = mode

class DecisionTree(BaseEstimator, ServingMixin):
    def __init__(self, depth, **kwargs):
        super().__init__(**kwargs)
        self.depth = depth

dt = DecisionTree(name='Request Time DT', depth=1, mode='online')
print(dt)
> name: Request Time DT, mode: online, depth: 1

dt.mode
> 'online'

Именно поэтому мы можем захотеть вызвать super() в базовом классе.

Ссылки
  • Вызов super() в родительском классе
  • Python super() с методами __init__()
  • super() в Python — это супер!

Когда использовать миксины?


Mixin — это класс, предоставляющий реализации методов для повторного использования дочерними классами. Он представляет ограниченную форму множественного наследования и родительский класс, который просто даёт функциональные возможности подклассам, не содержит состояния и не предназначен для создания экземпляров. Scikit-learn широко использует миксины. Это ClassifierMixin, TransformerMixin, OutlierMixin и т. д.

Когда использовать миксины? Они подходят, когда хочется:

  • предоставить множество дополнительных функций для класса;
  • использовать определённую функцию во множестве разных классов.

Ниже — пример первого случая. Начнём с создания базового объекта запроса в werkzeug:

from werkzeug import BaseRequest

class Request(BaseRequest):
    pass

Если нужно добавить поддержку заголовка accept, перепишем код вот так:

from werkzeug import BaseRequest, AcceptMixin

class Request(AcceptMixin, BaseRequest):
    pass

Нужна поддержка пользовательского агента, аутентификации и т. д.? Нет проблем, просто добавьте миксины:

from werkzeug import BaseRequest, AcceptMixin, UserAgentMixin, AuthenticationMixin

class Request(AcceptMixin, UserAgentMixin, AuthenticationMixin, BaseRequest):
    pass

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

Ссылки
  • Миксины Python
  • Что такое миксин и чем он полезен?

Использование относительного импорта


Относительный импорт гарантирует, что мы ищем текущий пакет (и импортируем из него) перед поиском в остальной части PYTHONPATH. Он работает, если перед импортом поставить . — вот пример из base.py от Sklearn:

from .utils.validation import check_X_y
from .utils.validation import check_array

Что произойдёт, если base.py не использует относительный импорт? Если у нас есть пакет с именем utils в каталоге нашего скрипта, во время импорта Python будет искать наш пакет utils, а не utils от sklearn, тем самым нарушая работу sklearn. Точка в выражении импорта гарантирует, что base.py в sklearn сначала ищет utils от Sklearn.

Есть ли другая причина не использовать относительный импорт? Пожалуйста, оставьте комментарий ниже.

Ссылки
  • Решение Гвидо об относительном импорте
  • Синтаксис from .package import ...

Когда добавлять код в __init__.py


__init__.py помечает каталоги как каталоги пакетов Python. Обычная практика — оставлять __init__.py пустыми. Тем не менее во многих библиотеках, которые я читал, были непустые, а иногда длинные файлы __init__.py. Это заставило меня задуматься, что и почему можно добавить в __init__.py.

Во-первых, мы можем добавить в __init__.py импорт, когда хотим реорганизовать код, который вырос в несколько модулей, без критических изменений для существующих пользователей. Допустим, у нас есть один модуль (models.py), который содержит реализацию для DecisionTree и Bandit. Со временем этот единственный модуль превращается в пакет моделей с модулями для tree и bandit. Чтобы обеспечить согласованность API для существующих пользователей, в __init__.py в пакете моделей мы можем добавить следующее:

from .tree import DecisionTree, RandomForest
from .bandit import Bandit, TSBandit

Это гарантирует, что существующие пользователи смогут продолжить импорт через from models import DecisionTree, а не from models.tree import DecisionTree. Для них API не меняется, а существующий код не ломается.

Это подводит к ещё одной причине добавить код в __init__.py, а именно — предоставить упрощённый API, чтобы пользователям не приходилось вникать в детали реализации:

app
  __init__.py
  model_implementation.py
  data_implementation.py

Вместо того чтобы заставлять пользователей решать, что импортировать из model_implementation и data_implementation, мы можем всё упростить, добавив в __init__.py такой код:

from .model_implementation import SimpleModel
from .data_implementation import SimpleDataLoader

В нём говорится, что SimpleModel и SimpleDataLoader — единственные части приложения, с которыми должны работать пользователи, и это упрощает использование пакета приложения (например, from app import SimpleModel, SimpleDataLoader). И, если пользователи знают, что делают, и хотят импортировать напрямую из model_implementation, то это тоже возможно.

Так делается в Pandas, где __init__.py импортируются типы данных, считыватели (reader) и API изменения формы, а ещё так делается в Accelerate от Hugging Face.

Помимо упомянутого выше применения, мы также можем захотеть:

  • инициализировать логгер в __init__.py основного пакета, чтобы использовать его в нескольких модулях;
  • выполнить проверки совместимости.

Ссылки
  • Для чего нужен __init__.py?
  • Что добавить в __init__.py?
  • Что у вас в __init__.py?

Когда использовать экземпляр, класс и статические методы


Краткий обзор методов, которые можно реализовать для класса. Такие методы пригодятся, когда:

  • Методам экземпляра нужен экземпляр класса, и доступ к нему они могут получить через self.
  • Методам класса не нужен экземпляр. Таким образом, они не могут получить доступ к экземпляру (self), но им доступен к класс cls
  • Статические методы не имеют доступа к self или cls. Они работают как обычные функции, но относятся к пространству имён классов.

Когда использовать класс или статические методы? Ниже — несколько основных рекомендаций, которые я нашёл.

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

Статические методы используются, когда нам не нужны аргументы класса или экземпляра, но некий метод связан с классом и удобно, чтобы этот метод находился в пространстве имён класса. Это могут быть специфичные для класса служебные методы. Оформляя метод как статический, мы улучшаем читаемость и понимание кода, сообщаем, что метод не зависит от класса или экземпляра.

Ссылки
  • Методы класса Python и статические методы Python
  • Есть пример использования метода класса Python?
  • Значение @classmethod и @staticmethod для начинающих?
  • Зачем использовать @staticmethod?
  • Демистификация экземпляра, класса и статических методов Python

Скрытая функция conftest.py


Обычно conftest.py используется для предоставления фикстур для всего каталога. После определения фикстуры в conftest.py, они могут использоваться любым тестом в пакете без необходимости импортировать их. Этот файл также используется для загрузки внешних плагинов и определения хуков, как setup и teardown.

Но, просматривая sklearn, я наткнулся на пустой conftest.py с этим интересным комментарием:

# Даже если этот файл пустой, он полезен тем, что при запуске из корневой папки
# ./sklearn добавляется в sys.path с помощью pytest. Смотрите
# https://docs.pytest.org/en/latest/explanation/pythonpath.html.
# Например, это позволяет создавать расширения на месте и запускать pytest
# doc/modules/clustering.rst, а также использовать sklearn из локальной папки, а не только из site-packages.

Оказывается, sklearn использовал менее известную функцию conftest.py: его существование в корневом пути гарантирует, что pytest распознает модули без необходимости указывать PYTHONPATH. В фоновом режиме pytest изменяет sys.path, включая все найденные в корневом пути подмодули.

Ссылки
  • механизмы импорта pytest и sys.path/PYTHONPATH
  • Для чего в pytest используются файлы conftest.py?

Поясняющая документация библиотек


Помимо чтения кода, мы можем учиться, читая документы, объясняющие библиотеку. Давайте сосредоточимся на принципах проектирования библиотек.

Принципы проектирования Scikit-learn включают:

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

В результате большинство моделей машинного обучения и преобразователей данных имеют метод fit(). Кроме того, модели машинного обучения имеют метод predict(), а преобразователи данных — метод transform(). Эта последовательность и простота способствуют лёгкости работы со sklearn. Принцип композиции также объясняет, почему sklearn построен на множественном наследовании базовых классов и миксинов.

Другой пример — fastai, где используется многоуровневый подход. Фреймворк предоставляет высокоуровневый API, готовые к использованию функции обучения моделей для различных приложений. Высокоуровневый API построен на основе иерархии низкоуровневых API, последние предоставляют компонуемые стандартные блоки. Этот многоуровневый подход позволяет через настройку API-интерфейсов среднего уровня быстро создать прототип перед его кастомизацией.

Изображение

Поделился принципами проектирования и PyTorch. Это:

  • предоставление прагматичной производительности;
  • чем хуже, тем лучше.

Первый принцип утверждает: чтобы быть полезной, библиотека должна обеспечивать убедительную производительность, но не за счёт простоты использования. Таким образом, ради значительно более простой в использовании библиотеки PyTorch готов пожертвовать 10, но не 100% скорости. Последний принцип говорит о том, что лучше иметь простое, но немного неполное решение, чем комплексный, но сложный в обслуживании дизайн.

• • •

Вот некоторые необычные способы работы с Python, о которых я узнал, читая эти билиотеки:

  • requests,
  • flask,
  • fastapi,
  • scikit-learn,
  • pytorch,
  • fastai,
  • pydantic,
  • django.

Я уверен, что пока смотрю поверхностно. Может быть, я что-нибудь упустил? Пожалуйста, оставьте комментарий ниже!





А мы поможем прокачать ваши навыки или с самого начала освоить профессию, востребованную в любое время:

  • Профессия Data Scientist (24 месяца)
  • Профессия Data Analyst (24 месяца)
  • Профессия Fullstack-разработчик на Python (15 месяцев)

Источник: https://habr.com/ru/company/skillfactory/blog/683744/


Интересные статьи

Интересные статьи

Меня зовут Александр, я руковожу backend-разработкой в КТS. Сегодня расскажу, как написать асинхронный краулер.Такая задача часто встречается на практике, когда нужно реализовать периодическую синхрон...
Мы уже говорили об автоматизации тестирования, теперь пришло время познакомиться с шестью лучшими инструментами автоматизации тестирования на Python.Есть хорошая новость ...
Одним из современных архитектурных подходов в области облачных вычислений является, так называемый, Serverless. Этот способ запуска приложений в облаке освобождает разраб...
Здравствуйте дорогие читатели. Этот канал посвящён программированию и робототехнике. И конечно он затрагивает образовательный аспект изучению этих дисциплин.Данная статья...
Добрый день. Наверное, все смотрели фильмы про железного человека и хотели себе голосового помощника, похожего на Джарвиса. В этом посте я расскажу, как сделать такого ассистента с ну...