Вы когда-нибудь задумывались, почему некоторые классы Python вызывают методы из ниоткуда? Или реализовать какие-то методы просто для передачи?

Если вы когда-либо сталкивались с пользовательским преобразователем Scikit-Learn, вы, скорее всего, знакомы с описанным выше явлением. Что ж, если это так, то эта статья для вас. Мы углубимся в концепцию под названием Полиморфизм, которая обеспечивает такое поведение, и создадим несколько пользовательских классов, чтобы набраться опыта и лучше понять.

Преобразователи Scikit-learn — это отличный набор инструментов для настройки конвейера подготовки данных в производственной среде. Хотя список встроенных преобразователей довольно исчерпывающий, создание собственного преобразователя — отличный способ автоматизировать преобразование пользовательских функций и экспериментировать. Если вы когда-либо работали с преобразователем scikit-learn, вы, скорее всего, сталкивались с широко используемым шаблоном:

# defining a custom transformer
    class CustomTransformer(BaseEstimator, TransformerMixin):
        def fit(self, X, y=None):
            return self  
        def transform(self, X):
            ...... 
# calling fit_transform() 
  customTransformer = CustomTransformer()
  data = customTransformer.fit_transform(data)

Но если вы не занимаетесь программированием, может показаться немного странным, что метод fit_transform() не был определен в классе CustomTransformer, но при этом его можно было вызывать из этого класса. Более того, если вы выяснили, что метод пришел из одного из вышеперечисленных классов, все же может показаться озадачивающим, как он может использовать методы, которые не были определены внутри того же класса, к которому он принадлежит. Например, проверьте скрипт класса TransformerMixin в официальном репозитории GitHub здесь, вы не найдете никаких методов, определенных для fit() или transform() внутри класса TransformerMixin.

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

Что такое Полиморфизм?

В общих чертах Полиморфизм означает способность объекта принимать различные формы. Конкретным примером может быть ситуация, когда объекты разных классов будут содержать одни и те же методы, но вести себя по-разному.

Например, в Python мы можем выполнять такие операции, как 1+2 или 'a' + 'b', и получать результаты 3 и ab соответственно. За кулисами Python вызывает магический метод с именем __add__(), который уже реализован в классах строк и целых чисел. Подробнее о том, как Python преобразует этот основной синтаксис в специальные методы, см. в моем последнем посте о Python Core Syntax.



Этот волшебный метод — __add__() — пример полиморфизма, где это один и тот же метод, но в зависимости от того, из какого объекта класса он вызывается, он меняет свое поведение от суммирования чисел до объединения строк.

В контексте класса Python мы можем добиться полиморфизма двумя способами: наследование и утиная типизация.

Полиморфизм через наследование

Наследование в контексте объектно-ориентированного программирования означает, что мы наследуем или получаем свойства класса от другого класса. Класс, от которого мы наследуем, называется Суперкласс, а свойства, от которых мы наследуем, называются Подкласс. Поскольку основное внимание в этой статье уделяется не наследованию, мы перейдем к примерам и, надеюсь, концепция будет иметь смысл по ходу дела.

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



Для нашего примера мы создадим суперкласс с именем InheritList и три подкласса: DefaultList, EvenList и OddList для запуска примеров наследования и полиморфизма.

Наследование

Обратите внимание, что в приведенном выше блоке кода мы не реализовали никаких методов внутри класса DefaultList. И обратите внимание в следующем блоке кода, как еще мы можем вызывать методы (например, add_value(), get_list()) из экземпляра, созданного из класса. Потому что подкласс DefaultList унаследовал эти методы от своего суперкласса — InheritList. Это наследование при play.nums = [1, 2, 3, 4, 5]

defaultNumList = DefaultList()
[defaultNumList.add_value(i) for i in nums]
print(f"List with all added values: {defaultNumList.get_list()}")​
# removes the last item from the list
defaultNumList.remove_value()
print(f"List after removing the last item: {defaultNumList.get_list()}")
>>List with all added values: [1, 2, 3, 4, 5]
>>List after removing the last item: [1, 2, 3, 4]

В приведенном выше примере показано базовое наследование — мы получаем все свойства от суперкласса и используем их как есть. Но мы могли бы изменить или обновить методы, унаследованные внутри подкласса, как мы сделали это в двух других подклассах — EvenList и OddList.

Переопределение метода

В классах EvenList и OddList мы модифицировали метод remove_value() так, чтобы класс EvenList удалял все нечетные значения, а OddList удалял все четные значения из построенного списка. Тем самым мы введем полиморфизм, где remove_value() будет вести себя по-разному в двух случаях.

>>evenNumList with all the values: [1, 2, 3, 4, 5]
>>evenNumList after applying remove_value(): [2, 4]

>>oddNumList with all the values: [1, 2, 3, 4, 5]
>>oddNumList after applying remove_value(): [1, 3, 5]

Полиморфизм через утиную типизацию

Прежде чем углубляться в утиную типизацию, давайте поговорим о другом методе do_all(), который был реализован в суперклассе — InheritList. Который принимает значение в качестве входных данных, добавляет его в список, удаляет ненужные значения из списка и возвращает окончательный список. Для выполнения всех этих задач требуются другие внутренние методы: add_value(), remove_value() и get_list(). Посмотрите демо ниже.

print(f"evenNumList after calling do_call(58): {evenNumList.do_all(58)}")
print(f"oddNumList after calling do_call(58): {oddNumList.do_all(55)}")
>>evenNumList after calling do_call(58): [2, 4, 58, 58]
>>oddNumList after calling do_call(58): [1, 3, 5, 55, 55]

Но Python позволяет нам реализовать это более гибко. Например, мы могли бы полностью удалить метод remove_value() из суперкласса, создать отдельный класс только с методом combine_all() и при этом иметь возможность использовать его без каких-либо проблем. Все благодаря Duck Typing!

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

«Если он ходит, как утка, плавает, как утка, и крякает, как утка, то, вероятно, это утка».

Чтобы продемонстрировать, давайте создадим новый класс с именем ComboFunc только с одним методом — combine_all(), который будет выполнять те же функции, что и метод do_all(). Кроме того, давайте создадим новый подкласс, который будет иметь один из ранее созданных подклассов — EvenList и этот новый класс в качестве суперклассов.

Обратите внимание, что мы не определили ни один из методов зависимостей (add_value(), remove_value() и get_list()) ни в одном из классов. И все же мы сможем успешно вызывать метод combine_all() из экземпляра класса GenDuckList. Потому что методы зависимостей будут унаследованы от класса EvenList, а метод combine_all() не заботится о том, откуда они берутся, пока они существуют.

>>Initial list: [1, 2, 3, 4, 5]
>>Final list: [2, 4, 40]

Обратите внимание, что мы могли бы выполнить указанную выше задачу и другими способами.

  1. Мы также могли бы полностью избежать наследования чего-либо от класса EvenList и реализовать методы зависимостей внутри класса, если бы нам нужно было что-то настроить. Или,
  2. Мы могли бы оставить его как суперкласс и при этом переопределить любые конкретные методы зависимостей, чтобы сделать его более индивидуальным. В целом, полиморфизм позволил нам стать более гибкими и легко повторно использовать уже реализованные методы. Или,
  3. Мы могли бы удалить remove_value() из суперкласса и реализовать его внутри нашего класса GenDuckList, но при этом иметь возможность выполнять те же задачи.

Итак, чтобы завершить круг, когда мы создаем собственный преобразователь в scikit-learn, используя классы BaseEstimator и TransformerMixin в качестве суперклассов, мы в основном применяем утиную типизацию для реализации полиморфизма. Чтобы связать, вы можете думать о GenDuckList как о фиктивном пользовательском классе-трансформере, ComboFunc как фиктивном классе TransformerMixin и EvenList как фиктивном классе BaseEstimator. Разница на уровне реализации между приведенным выше примером утиной типизации и примером с преобразователем в начале заключается в том, что мы унаследовали метод remove_value() от суперкласса, тогда как в пользовательском преобразователе мы определяем его внутри пользовательского класса — третий альтернативный способ, упомянутый выше.

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