Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs
На этом занятии
мы с вами познакомимся с декоратором singledispatch модуля functools, который
позволяет специализировать функции в зависимости от типа первого аргумента.
Давайте рассмотрим работу этого декоратора на простом примере.
Пусть имеется
функция с одним параметром, которая вычисляет сумму для переданной ей
итерируемой последовательности:
import functools
@functools.singledispatch
def my_sum(arg):
print("Вызов общей функции")
return sum(arg)
Эта функция
сразу декорирована с помощью singledispatch. Сам по себе
декоратор пока никак не меняет поведение этой функции. Она будет корректно
работать с любыми итерируемыми числовыми последовательностями:
res1 = my_sum([1, 2, 3])
print(res1)
Однако если ей
передать одно число или словарь:
res1 = my_sum([1, 2, 3])
res2 = my_sum({'a': 1, 'b': 2}) # ошибка
res3 = my_sum(1) # ошибка
print(res1, res2, res3)
то получим
ошибки применения функции sum() к таким данным. Выйти из этой
ситуации можно разными способами. Один из них, как раз предоставляет декоратор singledispatch. Мы можем
описать разные реализации функции my_sum() в зависимости
от типа первого аргумента. Например, так:
@my_sum.register(int)
def _(arg):
print("Сумма для типа int")
return arg
@my_sum.register(dict)
def _(arg):
print("Сумма для типа dict")
return sum(arg.values())
Метод register() регистрирует
объекты-функции для указанных типов данных первого аргумента. Имена
регистрируемых функций не имеют значения, они могут быть любыми. Поэтому я
просто записал символ подчеркивания. В результате, если переданное значение
будет целым числом, то оно будет просто возвращаться функцией. А если
передается словарь, то суммируются его значения, а не ключи. Теперь вызовы
функции:
res1 = my_sum([1, 2, 3])
res2 = my_sum({'a': 1, 'b': 2})
res3 = my_sum(1)
print(res1, res2, res3)
будут работать в
соответствии с их специализацией по типам первого аргумента. Если
зарегистрированный тип не находится, то используется первоначальная функция с
общим функционалом.
После выполнения
программы в консоли увидим строчки:
Вызов
общей функции
Сумма
для типа dict
Сумма
для типа int
6 3 1
Как видите,
теперь никаких ошибок нет и суммы подсчитаны верно.
Давайте
детальнее посмотрим на работу декоратора singledispatch. При первом
вызове он формирует объект со словарем dispatch_cache, в который по ключу object добавляет
ссылку на функцию my_sum(). Почему именно такой ключ для общей
функции, вы сейчас поймете. Два последующих вызова функции register() добавляют в
этот словарь еще две записи с ключами типов int и dict и
соответствующими ссылками на функции. После этого, при вызове функции:
начинается поиск
по словарю dispatch_cache на соответствие типа первого аргумента,
зарегистрированным типам. Вначале поиск осуществляется по специализированным
типам int и dict и проверяется с
помощью функции isinstance(). Если она возвращает True, соответствие
найдено, иначе поиск продолжается. В данном случае значение True будет получено
для типа object, который
соответствует начальной, общей функции. Поэтому она и вызывается для списка.
Я напомню, что
функция isinstance() проверяет
типы с учетом их иерархии наследования. И, так как, все типы неявно наследуются
от object, то это есть
общий универсальный тип данных в языке Python.
Когда вызывается
функция:
res2 = my_sum({'a': 1, 'b': 2})
происходит все
то же самое, только соответствие находится для ключа dict с выполнением
соответствующей специализированной функции. И так для всех зарегистрированных
функций.
Обратите
внимание, что тип для проверки берется именно у первого переданного аргумента в
функцию. Остальные аргументы (вторые, третьи и т.д.) могут иметь любые типы
данных. Это никак не скажется на выборе специализированных функций. Например,
пусть имеется следующая функция:
@functools.singledispatch
def my_per(a, b):
print("Вызов общей функции")
return a + b
Зарегистрируем
для нее специализацию сразу для двух типов list и set:
@my_per.register(list | set)
def _(arg):
print("Периметр для типов list и set")
return sum(arg)
И еще для типа float:
@my_per.register(float)
def _(a, b):
print("Периметр для типа float")
return a + b
Теперь, если
вызвать эти функции командами:
res1 = my_per(1, 2.5)
res2 = my_per([1, 2, 3])
res3 = my_per({10, 20})
то в первом
случае будет вызвана общая функция, а не специализированная для float, несмотря на
то, что второй аргумент имеет этот тип данных. А вот если первый аргумент будет
иметь тип float:
то вызовется
специализация для этого типа данных. Это следует иметь в виду, используя декоратор
singledispatch.
Помимо
стандартных (встроенных) типов данных, можно использовать свои собственные,
например, в виде объявленных классов. Давайте, объявим два класса Perimetr и Area
для двух разных типов, следующим образом:
class Perimetr:
def __init__(self, a, b):
self.a = a
self.b = b
class Area:
def __init__(self, a, b):
self.a = a
self.b = b
А следом
определим специализации функции, используя эти классы:
@functools.singledispatch
def my_calc_fig(arg):
print("Вызов общей функции")
return arg
@my_calc_fig.register(Perimetr)
def _(a):
print("Периметр, тип Perimetr")
return (a.a + a.b) * 2
@my_calc_fig.register(Area)
def _(a):
print("Площадь, тип Area")
return a.a * a.b
Если теперь
вызвать функцию my_calc_fig() командами:
res1 = my_calc_fig(Perimetr(2, 6))
res2 = my_calc_fig(Area(2, 6))
то в первом
случае будет вычислен периметр, а во втором – площадь прямоугольника.
Таким образом, использование
декоратора singledispatch полезно в ситуациях, когда одна функция должна
обрабатывать разные типы данных различными способами, сохраняя общий интерфейс
вызова.
Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs