Правильный способ перегрузки функций в Python
Last updated
Was this helpful?
Last updated
Was this helpful?
Оригинал статьи:
Вас учили, что в Python невозможна перегрузка функций? Вот как вы можете сделать это с помощью универсальных функций и множественной отправки!
Перегрузка функций — это распространенный шаблон программирования, который, по-видимому, зарезервирован для компилируемых языков со статической типизацией. Тем не менее, есть простой способ реализовать это на Python с помощью Multiple Dispatch или, как это называется в Python, мультиметоды.
Перво-наперво — вы можете спросить, как мы можем реализовать перегрузку методов в Python, когда мы все знаем, что это невозможно? Что ж, несмотря на то, что Python является языком с динамической типизацией и, следовательно, не может иметь правильную перегрузку методов, поскольку для этого требуется, чтобы язык мог различать типы во время компиляции, мы все же можем реализовать его немного другим способом, подходящим для динамически-типизированных языков.
Этот подход называется Multiple Dispatch или multimethods, когда интерпретатор различает несколько реализаций функции/метода во время выполнения на основе динамически определяемых типов. Чтобы быть более точным, язык использует типы аргументов, передаваемых функции во время ее вызова, чтобы динамически выбирать, какую из нескольких реализаций функций использовать (или отправлять).
Теперь вы можете подумать: «Нам действительно это нужно? Если его невозможно реализовать нормально, возможно, нам не следует использовать его в Python…» Да, верное замечание, но есть веские причины реализовать некоторую форму перегрузки функций/методов в Python. Это мощный инструмент, который может сделать код более кратким, читабельным и свести к минимуму его сложность. Однако без мультиметодов «очевидный способ» сделать это — использовать проверку типов с помощью isinstance()
. Это очень уродливое, хрупкое решение, закрытое для расширения, и я бы назвал его анти-паттерном.
Кроме того, в Python уже существует перегрузка методов для таких операторов и методов, как len()
или new()
, с использованием так называемых методов dunder или magic (см. документацию ), и мы все используем это довольно часто, так почему бы не использовать правильную перегрузку для всех функций, верно?
Итак, теперь мы знаем, что можем реализовать перегрузку в Python, так как именно мы это делаем?
Выше мы говорили о Multiple Dispatch, но Python не поддерживает это из коробки, или, другими словами, Multiple Dispatch не является функцией стандартной библиотеки Python. Однако то, что нам доступно, называется Single Dispatch, поэтому давайте сначала начнем с этого более простого случая.
Единственная фактическая разница между мульти- и одиночной отправкой — это количество аргументов, которые мы можем перегрузить. Итак, для этой реализации в стандартной библиотеке он всего один.
Функция (и декоратор), обеспечивающая эту функцию, называется singledispatch
и находится в .
Всю эту концепцию лучше всего пояснить на нескольких примерах. Есть много «академических» примеров перегрузки функций (геометрические фигуры, сложение, вычитание…), которые мы, вероятно, уже видели. Вместо того, чтобы повторяться, давайте рассмотрим несколько практических примеров. Итак, вот первый пример для singledispatch
для форматирования даты, времени и datetime:
Начнем с определения базовой функции format
, которая будет перегружена. Эта функция украшена @singledispatch
и обеспечивает базовую реализацию, которая используется, если нет лучших вариантов. Затем мы определяем отдельные функции для каждого типа, который мы хотим перегрузить — в данном случае date
, datetime
и time
— каждая из них имеет имя _
(подчеркивание), потому что они все равно будут вызываться (отправляться) через метод format
, поэтому нет необходимости чтобы дать им полезные имена. Каждый из них также украшен @format.register
, который связывает их с ранее упомянутой функцией format
. Затем, чтобы можно было различать типы, у нас есть два варианта: мы можем использовать аннотации типов, как показано в первых двух случаях, или явно добавить тип в декоратор, как в последнем случае из примера.
В некоторых случаях может иметь смысл использовать одну и ту же реализацию для нескольких типов — например, для числовых типов, таких как int
и float
— для этих ситуаций разрешено стекирование декораторов, что означает, что вы можете перечислить (сложить) несколько @format.register(type)
строк, чтобы связать функцию со всеми допустимыми типами.
Этот модуль и его декоратор — @dispatch
— ведут себя очень похоже на @singledispatch
в стандартной библиотеке. Единственное фактическое отличие состоит в том, что он может принимать несколько типов в качестве аргументов:
В приведенном выше фрагменте показано, как мы можем использовать декоратор @dispatch
для перегрузки нескольких аргументов, например, для реализации конкатенации различных типов. Как вы, наверное, заметили, с библиотекой multipleddispatch нам не нужно было определять и регистрировать базовую функцию, вместо этого мы создавали несколько функций с одинаковыми именами. Если бы мы хотели предоставить базовую реализацию, мы могли бы использовать @dispatch(object, object)
, который перехватывал бы любые неспецифические типы аргументов.
В предыдущих примерах показано доказательство концепции, но если бы мы действительно хотели реализовать такую функцию конкатенации concatenate, нам нужно было бы сделать ее гораздо более общей. Это можно решить с помощью типов объединения. В этом конкретном примере мы могли бы изменить первую функцию следующим образом:
Это сделало бы так, чтобы первый аргумент функции мог быть любым из list или tuple, а второй был бы str или int. Это уже намного лучше, чем предыдущее решение, но его можно еще улучшить, используя абстрактные типы. Вместо перечисления всех возможных последовательностей мы можем использовать абстрактный тип Sequence (при условии, что наша реализация может его обрабатывать), который охватывает такие вещи, как list, tuple или range:
Если вы хотите использовать этот подход, то полезно взглянуть на модуль collections.abc
и посмотреть, какой тип данных контейнера лучше всего соответствует вашим потребностям. В основном, чтобы убедиться, что ваша функция сможет обрабатывать все типы, попадающие в выбранный контейнер.
Все это смешивание и сопоставление типов аргументов удобно, но также может вызвать неоднозначность при выборе подходящей функции для определенного набора параметров. К счастью, multipleddispatch предоставляет AmbiguityWarning, которое возникает, если возможно неоднозначное поведение:
В этой статье мы рассмотрели простую, но мощную концепцию, которую я редко вижу в Python, и это позор, учитывая, что она может значительно улучшить читаемость кода и избавиться от антипаттернов, таких как проверка типов с помощью isinstance()
. Кроме того, я надеюсь, вы согласитесь с тем, что этот подход к перегрузке функций следует считать «очевидным способом», и я надеюсь, что вы воспользуетесь им при необходимости.
Помимо возможности перегружать базовые функции, модуль functools
содержит также метод , который можно применять к методам класса. Примером этого может быть следующее:
Часто Single Dispatch будет недостаточно, и вам может понадобиться надлежащая функциональность Multiple Dispatch. Это доступно из модуля multipleddispatch
, который можно найти , и его можно установить с помощью pip install multipleddispatch
.
Если вы хотите глубже погрузиться в эту тему и запачкать руки, вы можете сами реализовать мультиметоды, как показано в — это может быть хорошим упражнением, чтобы понять, как на самом деле работает множественная диспетчеризация.
Наконец, я также, вероятно, должен упомянуть, что в этой статье опущены примеры известной , о которых я упоминал в начале, а также некоторые подходы к перегрузке конструкторов, например, с использованием . Итак, если это то, что вы ищете, перейдите по этим ссылкам/ресурсам, которые дают хороший обзор по этим темам.