Python содержит удобный модуль под названием functools. Функции внутри functools можно определить как функции высокого порядка, которые взаимодействуют и возвращают другие функции. В этой статье мы рассмотрим содержимое пакета functools:
- lru_cache
- partials
- singledispatch
- wraps
Давайте начнем с изучения создания простого кэша в Python.
Кэширование с functools.lru_cache
Модуль functools содержит весьма полезный декоратор под названием lru_cache. Обратите внимание на то, что он был добавлен в версии Python 3.2. Соответственно документации, этот декоратор «оборачивает функцию вызываемым запоминанием, которое сохраняет максимальное количество всех последних вызовов«. Другими словами, это декоратор, который добавляет кэширование к декорируемой функции. Давайте напишем быструю функцию, которая основана на примере из документации functools и охватывает кое-какие веб страницы. В нашем случае, мы охватим страницы из сайта документации Python.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# -*- coding: utf-8 -*- import urllib.error import urllib.request from functools import lru_cache @lru_cache(maxsize=24) def get_webpage(module): """ Поиск странице модуля """ webpage = "https://docs.python.org/3/library/{}.html".format(module) try: with urllib.request.urlopen(webpage) as request: return request.read() except urllib.error.HTTPError: return None if __name__ == '__main__': modules = ['functools', 'collections', 'os', 'sys'] for module in modules: page = get_webpage(module) if page: print("{} страница модуля найдена!".format(module)) |
В данном коде мы декорируем нашу функцию get_webpage при помощи lru_cache и указываем максимальный размер в 24 вызова. Далее мы устанавливаем переменную строки веб странице и передаем тот модуль, который нужно получить. На практике я обнаружил, что это работает лучше всего, если выполнить запуск в интерпретаторе Python, в таком как IDLE. Это позволит вам запустить цикл несколько раз над функцией Python. Когда вы запустите код, первое что вы заметите, что выдача выводится сравнительно медленно. Но если вы запустите его еще раз в той же сессии, вы увидите, что выдача появится мгновенно, что оговорит о том, что lru_cache кешировал вызовы корректно. Попробуйте сделать это лично в своем интерпретаторе, чтобы увидеть результат. Также существует типизированный параметр, который мы можем передать декоратору. Это Boolean, указывающий декоратору кешировать аргументы разных типов раздельно, если для типизации задано значение True.
functool.partial
Один из классов functools называется partial. Вы можете использовать для создания новой функции с частичным приложением аргументов и ключевых слов, которые вы передаете. Вы также можете использовать partial для «заморозки» части аргументов вашей функции и\или ключей, которые отображаются в новом объекте. Еще один способ применения partial это создание функции с разными настройками. Давайте взглянем на пример:
1 2 3 4 5 6 7 8 |
from functools import partial def add(x, y): return x + y p_add = partial(add, 2) p_add(4) # 6 |
Здесь мы создали простую функцию добавления, которая возвращает результат добавленных ею аргументов x и y. Далее мы создаем новую вызываемую, создав экземпляр partial и передав его нашей функции, а также аргумент для этой функции. Другими словами, мы в целом присваиваем параметру х в нашей функции значение 2. Наконец, мы вызываем p_add с аргументом числа 4, что в результате дает 6, так как 2+4=6.
Еще одним удобным вариантом использования для partials является передача аргументов коллбекам. Девайте взглянем на пример, используя wx:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# -*- coding: utf-8 -*- import wx from functools import partial class MainFrame(wx.Frame): """ Приложение показывает несколько кнопок """ def __init__(self, *args, **kwargs): """Конструктор""" super(MainFrame, self).__init__(parent=None, title='Partial') panel = wx.Panel(self) sizer = wx.BoxSizer(wx.VERTICAL) btn_labels = ['one', 'two', 'three'] for label in btn_labels: btn = wx.Button(panel, label=label) btn.Bind(wx.EVT_BUTTON, partial(self.onButton, label=label)) sizer.Add(btn, 0, wx.ALL, 5) panel.SetSizer(sizer) self.Show() def onButton(self, event, label): """ Дожидаемся действия нажатия кнопки """ print('Вы нажали: ' + str(label)) if __name__ == '__main__': app = wx.App(False) frame = MainFrame() app.MainLoop() |
Здесь мы используем partial для вызова onButton, обработчика событий с дополнительным аргументом, который представлен в виде ярлыка кнопки. Это может выглядеть не слишком полезным, но если вы связанны с программированием графического интерфейса, вы заметите, как часто люди задаются вопросом «а как это сделать?». Конечно, вы также можете использовать lambda функцию вместо передачи аргументов коллбекам.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Один из способов применения используется в работе для автоматического теста Фреймворка. Мы тестируем пользовательский интерфейс при помощи Python и нам нужно быть в состоянии передать функцию, чтобы отменить определенные диалоги. Обычно вы можете передать функцию вместе с названием диалога для отмены, но для корректной работы его потребуется вызвать в конкретной точке в процессе. Так как я не могу показать тот код, вот очень простой пример передачи функции partial:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
from functools import partial def add(x, y): return x + y def multiply(x, y): return x * y def run(func): print(func()) def main(): a1 = partial(add, 1, 2) m1 = partial(multiply, 5, 8) run(a1) run(m1) if __name__ == "__main__": main() |
Здесь мы создаем несколько функций partial в нашей главной функции. Далее мы передаем их нашей функции run, вызываем её и затем выводим результат вызванной функции.
Перегрузка функции с functools.singledispatch
Совсем недавно в Python была добавлена поддержка partial для перегрузки функции в версии 3.4. Этот инструмент является аккуратным небольшим декоратором для модуля functools под названием singledispatch. Этот декоратор превращает вашу обычную функцию в функцию родовой рассылки. Однако обратите внимание на то, что singledispatch появляется только на основании типа первого аргумента. Давайте взглянем на пример, чтобы увидеть, как это работает.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
from functools import singledispatch @singledispatch def add(a, b): raise NotImplementedError('Unsupported type') @add.register(int) def _(a, b): print("First argument is of type ", type(a)) print(a + b) @add.register(str) def _(a, b): print("First argument is of type ", type(a)) print(a + b) @add.register(list) def _(a, b): print("First argument is of type ", type(a)) print(a + b) if __name__ == '__main__': add(1, 2) add('Python', 'Programming') add([1, 2, 3], [5, 6, 7]) |
Здесь мы импортировали singledispatch из functools и применили его в простой функцию, которую мы назвали add. Эта функция является всеохватывающей и может быть вызвана только в том случае, если никакие другие декорированные функции не обрабатывают переданный тип. Вы заметите, что мы обрабатываем целые числа, строки и списки как первый аргумент. Если мы вызовем нашу функцию add с чем-то еще, например, со словарем, то это приведет к ошибке NotImplementedError. Попробуйте запустить этот код самостоятельно. Вы увидите выдачу, которая выглядит примерно следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
First argument is of type <class 'int'> 3 First argument is of type <class 'str'> PythonProgramming First argument is of type <class 'list'> [1, 2, 3, 5, 6, 7] Traceback (most recent call last): File "overloads.py", line 30, in <module> add({}, 1) File "/usr/local/lib/python3.5/functools.py", line 743, in wrapper return dispatch(args[0].__class__)(*args, **kw) File "overloads.py", line 5, in add raise NotImplementedError('Unsupported type') NotImplementedError: Unsupported type |
Как вы видите, код работает именно так, как и ожидалось. Он вызывает подходящую функцию, основанную на первом типе аргумента. Если тип не обработан, то это приведет к ошибке NotImplementedError. Если вы хотите узнать, какие типы мы в данный момент обрабатываем, вы можете добавить следующую часть кода в конце файла, желательно перед строкой, которая вызывает ошибку:
1 |
print(add.registry.keys()) |
Это выведет что-то на подобии этого:
1 |
dict_keys([<class 'str'>, <class 'int'>, <class 'list'>, <class 'object'>]) |
Это говорит нам о том, что мы можем обрабатывать строки, целые числа, списки и объекты (по умолчанию). Декоратор singledispatch также поддерживает укладку. Это позволяет нам создавать перегруженную функцию, которая обрабатывается несколько раз. Давайте взглянем на пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from functools import singledispatch from decimal import Decimal @singledispatch def add(a, b): raise NotImplementedError('Unsupported type') @add.register(float) @add.register(Decimal) def _(a, b): print("First argument is of type ", type(a)) print(a + b) if __name__ == '__main__': add(1.23, 5.5) add(Decimal(100.5), Decimal(10.789)) |
Это, как правило, говорит Python о том, что одна из перегруженных функции add может обрабатывать типы float и decimal.Decimal в качестве первого аргумента. Если вы запустите этот код, вы увидите следующее:
1 2 3 4 5 6 7 |
First argument is of type <class 'float'> 6.73 First argument is of type <class 'decimal.Decimal'> 111.2889999999999997015720510 dict_keys([<class 'float'>, <class 'int'>, <class 'object'>, <class 'decimal.Dec\ imal'> |
Возможно, вы уже заметили, но из-за того, как эти функции были написаны, вы можете складывать декораторы для обработки всех случаев, как в предыдущем примере, так и в этом, в перегруженную функцию. В любом случае, в нормальном случае перегрузки, каждое форсирование вызывает другой код, вместо того, чтобы выполнять одну и ту же задачу.
functools.wraps
Мы рассмотрим не самый известный инструмент, под названием called, который также является частью модуля functools. Вы можете использовать его как декоратор для исправления docstrings и наименований декорированных функций.
Имеет ли это значение? Как минимум, звучит немного странно, но если вы пишете API, или любой другой код, который будет использоваться кем-нибудь другим, а не только вами, эта часть может быть весьма важной. Причина в том, что когда вы используете интроспекцию Python, что бы разобраться в чужом коде, декорированная функция выдаст неправильную информацию. Давайте взглянем на простой пример, который я продублировал из decorum.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
def another_function(func): """ Функция которая принимает другую функцию """ def wrapper(): """ Оберточная функция """ val = "The result of %s is %s" % (func(), eval(func()) ) return val return wrapper @another_function def a_function(): """Обычная функция""" return "1+1" if __name__ == "__main__": print(a_function.__name__) print(a_function.__doc__) |
В этом коде мы декорируем функцию под названием a_function с another_function. Вы можете проверить название функции и docstring, выведя их, используя свойства функции __name__ и __doc__ . Если вы запустите данный пример, вы получите следующую выдачу:
1 2 3 |
wrapper Оберточная функция |
Это не правильно! Если вы запустите эту программу IDLE или в интерпретаторе, станет понятно, насколько это может запутать.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import decorum help(decorum) Help on module decorum: NAME decorum - FILE /home/mike/decorum.py FUNCTIONS a_function = wrapper() Оберточная функция another_function(func) Функция принимает другую функцию |
1 |
wrapper() # Оберточная функция |
В целом, здесь происходит следующее: декоратор меняет название декорированной функции и docstring на свое собственное.
Спасение во wraps!
Как исправить этот бардак? Разработчики Python предоставили нам отличное решение в лице functools.wraps! Давайте посмотрим:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
from functools import wraps def another_function(func): """ Функция которая принимает другую функцию """ @wraps(func) def wrapper(): """ Оберточная функция """ val = "The result of %s is %s" % (func(),eval(func())) return val return wrapper @another_function def a_function(): """Обычная функция""" return "1+1" if __name__ == "__main__": #a_function() print(a_function.__name__) print(a_function.__doc__) |
Здесь мы импортируем wraps из модуля functools и используем его в качестве декоратора для вложенной функции-обертки внутри another_function. Если вы запустите его сейчас, выдача изменится:
1 2 |
a_function Обычная функция |
Теперь все названия прописаны правильно. Если вы перейдете в интерпретатор Python, функция help также будет работать корректно. Я пропущу копирование выдачи здесь, и хочу, чтобы вы попробовали лично сделать это.
Подведем итоги
Давайте подумаем. В этой статье вы научились базовому кэшированию с использованием lru_cache. После этого мы изучили partial, который позволяет нам «замораживать» часть аргументов и\или ключей в вашей функции, позволяя вам создавать новый объект, который вам нужно вызвать. Далее, мы использовали singledispatch для перегрузки функций в Python. Так как это только позволяет функции перегрузиться на основании первого аргумента, этот инструмент может оказаться весьма кстати в будущем! Наконец, мы рассмотрели wraps, который обладает весьма узкой спецификой: исправление docstring и названий функций, которые были декорированы таким образом, что у них нет docstring декоратора, или названия.
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»