Перегрузка функций - это возможность иметь несколько функций с одним и тем же именем, но с разными сигнатурами / реализациями. Когда вызывается перегруженная функция fn, среда выполнения сначала оценивает аргументы / параметры, переданные вызову функции, и, судя по этому, вызывает соответствующую реализацию.

В приведенном выше примере (написанном на C ++) функция area перегружена двумя реализациями; один принимает два аргумента (оба целых числа), представляющие длину и ширину прямоугольника, и возвращает площадь; в то время как другая функция принимает целочисленный радиус круга. Когда мы вызываем функцию area, как area(7), она вызывает вторую функцию, а area(3, 4) - первую.

Почему в Python нет перегрузки функций?

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

Вызывая функцию locals() после определения функции, мы видим, что она возвращает словарь всех переменных, определенных в локальном пространстве имен. Ключ словаря - это имя переменной, а значение - это ссылка / значение этой переменной. Когда среда выполнения встречает другую функцию с тем же именем, она обновляет запись в локальном пространстве имен и, таким образом, исключает возможность сосуществования двух функций. Следовательно, python не поддерживает перегрузку функций. Это дизайнерское решение было принято при создании языка, но это не мешает нам его реализовать, поэтому давайте перегрузим некоторые функции.

Реализация перегрузки функций в Python

Мы знаем, как Python управляет пространствами имен, и если мы захотим реализовать перегрузку функций, нам потребуется

  • управлять определениями функций в поддерживаемом виртуальном пространстве имен
  • найти способ вызвать соответствующую функцию в соответствии с переданными ей аргументами

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

Завершение функции

Мы создаем класс с именем Function, который обертывает любую функцию и делает ее вызываемой через переопределенный метод __call__, а также предоставляет метод с именем key, который возвращает кортеж, который делает эту функцию уникальной для всей кодовой базы.

В приведенном выше фрагменте функция key возвращает кортеж, который однозначно идентифицирует функцию в базе кода и содержит

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

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

В приведенном выше примере функция area обернута в Function и создана в func. key() возвращает кортеж, первым элементом которого является имя модуля __main__, вторым - класс <class 'function'>, третьим - имя функции area, а четвертым - количество аргументов, которые принимает функция area, то есть 2.

Пример также показывает, как мы могли бы просто вызвать экземпляр func, точно так же, как обычную функцию area, с аргументами 3 и 4 и получить ответ 12, который мы и получили бы, если бы вызвали area(3, 4). Такое поведение пригодится на более позднем этапе, когда мы будем играть с декораторами.

Создание виртуального пространства имен

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

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

Использование декораторов в качестве зацепа

Теперь, когда мы определили виртуальное пространство имен с возможностью регистрации функции, нам нужен хук, который вызывается во время определения функции; а здесь используйте декораторы Python. В Python декоратор оборачивает функцию и позволяет нам добавлять новые функции к существующей функции без изменения ее структуры. Декоратор принимает обернутую функцию fn в качестве аргумента и возвращает другую функцию, которая вместо этого вызывается. Эта функция принимает args и kwargs, переданные во время вызова функции, и возвращает значение.

Пример декоратора, который показывает время выполнения функции, показан ниже.

В приведенном выше примере мы определяем декоратор с именем my_decorator, который обертывает функцию area и печатает stdout время, затраченное на выполнение.

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

Декоратор overload возвращает экземпляр Function, возвращенный .register() функцией пространства имен. Теперь всякий раз, когда вызывается функция (украшенная overload), она вызывает функцию, возвращаемую функцией .register() - экземпляр Function, а метод __call__ выполняется с указанными args и kwargs, переданными во время вызова. Теперь остается реализовать метод __call__ в классе Function таким образом, чтобы он вызывал соответствующую функцию с учетом аргументов, переданных во время вызова.

Поиск подходящей функции из пространства имен

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

Роль этой get функции - решить, какая реализация функции (если она перегружена) должна быть вызвана. Процесс получения соответствующей функции довольно прост - из функции и аргументов создайте уникальный ключ с помощью функции key (как это было сделано при регистрации) и посмотрите, существует ли он в реестре функций; если это так, то выберите реализацию, сохраненную против него.

Функция get создает экземпляр Function только для того, чтобы она могла использовать функцию key для получения уникального ключа, а не для копирования логики. Затем ключ используется для выборки соответствующей функции из реестра функций.

Вызов функции

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

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

Перегрузка функций в действии

После того, как весь код размещен, мы определяем две функции с именем area: одна вычисляет площадь прямоугольника, а другая вычисляет площадь круга. Обе функции определены ниже и украшены декоратором overload.

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

Заключение

Python не поддерживает перегрузку функций, но, используя общеязыковые конструкции, мы взломали решение этой проблемы. Мы использовали декораторы и поддерживаемое пользователем пространство имен для перегрузки функций и использовали количество аргументов как фактор устранения неоднозначности. Мы также могли бы использовать типы данных (определенные в декораторе) аргументов для устранения неоднозначности, что позволяет функциям с одинаковым количеством аргументов, но разными типами перегружаться. Степень детализации перегрузки ограничена только функцией getfullargspec и нашим воображением. Более аккуратный, чистый и более эффективный подход также возможен с вышеуказанными конструкциями, поэтому не стесняйтесь реализовывать одну и писать мне в Твиттере @arpit_bhayani, я буду рад узнать, что вы с ней сделали.

Если вам понравилось то, что вы читаете, подпишитесь на мой информационный бюллетень и получите сообщение прямо на ваш почтовый ящик, а также напишите мне привет @arpit_bhayani.

Прошлые статьи из моего информационного бюллетеня

Эта статья изначально была опубликована в моем блоге - Функции перегрузки в Python.