Рубрики
Без рубрики

Функции перегрузки в Python

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

Автор оригинала: Arpit Bhayani.

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

int area(int length, int breadth) {
  return length * breadth;
}

float area(int radius) {
  return 3.14 * radius * radius;
}

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

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

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

def area(radius):
  return 3.14 * radius ** 2

>>> locals()
{
  ...
  'area': ,
  ...
}

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

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

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

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

Обертывание функции

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

from inspect import getfullargspec

class Function(object):
  """Function is a wrap over standard python function.
  """
  def __init__(self, fn):
    self.fn = fn

  def __call__(self, *args, **kwargs):
    """when invoked like a function it internally invokes
    the wrapped function and returns the returned value.
    """
    return self.fn(*args, **kwargs)

  def key(self, args=None):
    """Returns the key that will uniquely identify
    a function (even when it is overloaded).
    """
    # if args not specified, extract the arguments from the
    # function definition
    if args is None:
      args = getfullargspec(self.fn).args

    return tuple([
      self.fn.__module__,
      self.fn.__class__,
      self.fn.__name__,
      len(args or []),
    ])

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

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

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

def area(l, b):
  return l * b

>>> func = Function(area)
>>> func.key()
('__main__', , 'area', 2)
>>> func(3, 4)
12

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

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

Построение виртуального пространства имен

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

class Namespace(object):
  """Namespace is the singleton class that is responsible
  for holding all the functions.
  """
  __instance = None

  def __init__(self):
    if self.__instance is None:
      self.function_map = dict()
      Namespace.__instance = self
    else:
      raise Exception("cannot instantiate a virtual Namespace again")

  @staticmethod
  def get_instance():
    if Namespace.__instance is None:
      Namespace()
    return Namespace.__instance

  def register(self, fn):
    """registers the function in the virtual namespace and returns
    an instance of callable Function that wraps the
    function fn.
    """
    func = Function(fn)
    self.function_map[func.key()] = fn
    return func

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

def area(l, b):
  return l * b

>>> namespace = Namespace.get_instance()
>>> func = namespace.register(area)
>>> func(3, 4)
12

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

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

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

import time


def my_decorator(fn):
  """my_decorator is a custom decorator that wraps any function
  and prints on stdout the time for execution.
  """
  def wrapper_function(*args, **kwargs):
    start_time = time.time()

    # invoking the wrapped function and getting the return value.
    value = fn(*args, **kwargs)
    print("the function execution took:", time.time() - start_time, "seconds")

    # returning the value got after invoking the wrapped function
    return value

  return wrapper_function


@my_decorator
def area(l, b):
  return l * b


>>> area(3, 4)
the function execution took: 9.5367431640625e-07 seconds
12

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

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

def overload(fn):
  """overload is the decorator that wraps the function
  and returns a callable object of type Function.
  """
  return Namespace.get_instance().register(fn)

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

Поиск нужной функции из пространства имен

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

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

def get(self, fn, *args):
  """get returns the matching function from the virtual namespace.

  return None if it did not fund any matching function.
  """
  func = Function(fn)
  return self.function_map.get(func.key(args=args))

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

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

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

def __call__(self, *args, **kwargs):
  """Overriding the __call__ function which makes the
  instance callable.
  """
  # fetching the function to be invoked from the virtual namespace
  # through the arguments.
  fn = Namespace.get_instance().get(self.fn, *args)
  if not fn:
    raise Exception("no matching function found.")

  # invoking the wrapped function and returning the value.
  return fn(*args, **kwargs)

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

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

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

@overload
def area(l, b):
  return l * b

@overload
def area(r):
  import math
  return math.pi * r ** 2


>>> area(3, 4)
12
>>> area(7)
153.93804002589985

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

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

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

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

Подпишитесь на рассылку новостей подмышек