{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Декораторы\n", "\n", "Отчасти по причине того, что в `python` функции --- это объекты первого порядка, в нем распространены то, что в других языках называется функциями высшего порядка, т.е. функции, которые принимают в качестве параметра или возвращают другие функции. Для конкретного вида таких функций в `python` добавили специальный элемент языка --- декораторы.\n", "\n", "## Декорация функции одного аргумента\n", "\n", "Предположим, что в целях отладки мы хотим печатать логирующее сообщение при каждом вызове функции `sin`. Мы можем прийти примерно к такому решению.\n" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Функция sin вызвана с аргументом 0\n", "0.0\n" ] } ], "source": [ "import math \n", "\n", "def sin(x):\n", " print(f\"Функция sin вызвана с аргументом {x}\")\n", " return math.sin(x)\n", "\n", "print(sin(0))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Такие конструкции называют обертками над функциями: пользовательская функция `sin` оборачивает функцию `math.sin`.\n", "\n", "Теперь, предположим, что мы захотим проделать то же самое и с другими функциями. Написание своей обертки для каждой из них приведет к излишнему дублированию кода. Чтобы этого избежать, напишем функцию `announce`, которая, принимая на вход некоторую функцию одного аргумента `func`, возвращает функцию обертку `wrap`. Функция обертка `wrap` в свою очередь вызывает `func`, но перед этим печатает её имя (`func.__name__`) и значение аргумента, с которым она была вызвана.\n", "\n", "Реализация этой функции может выглядеть как-то так." ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [], "source": [ "def announce(func):\n", " def wrap(x):\n", " print(f\"Функция {func.__name__} вызвана с аргументом {x}\")\n", " return func(x)\n", " return wrap" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Функция `announce` внутри тела объявляет новую функцию `wrap` и тем самым создаёт объект этой функции. Когда этот объект создан, он сразу возвращается наружу. Функция `wrap` в свою очередь является оберткой: она возвращает результат вычисления обертываемой функции `func`, но перед этим делает дополнительные действия, а именно печатает имя функции (атрибут `func.__name__`) и аргумент, с которым вызывается функция. С этим же аргументом вызывается функция `func`. \n", "\n", "Проверим работоспособность это функции. Для этого обернем функции синуса и косинуса." ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.0 1.0\n", "Функция sin вызвана с аргументом 0\n", "Функция cos вызвана с аргументом 0\n", "0.0 1.0\n" ] } ], "source": [ "from math import sin, cos\n", "\n", "print(sin(0), cos(0))\n", "\n", "sin = announce(sin)\n", "cos = announce(cos)\n", "\n", "print(sin(0), cos(0))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видно, что обертывание функций `sin` и `cos` приводит к желаемому эффекту.\n", "\n", "Такого вида функции и называют декораторами. При этом, т.к. такие функции часто применяются и к пользовательским функциям, то в `python` ввели специальный синтаксический сахар для их применения.\n", "\n", "Вместо комбинации инструкций\n", "```python\n", "def my_function(x):\n", " pass\n", "\n", "my_function = announce(my_function)\n", "```\n", "можно смело писать\n", "```python\n", "@announce\n", "def my_function(x):\n", " pass\n", "```\n", "Они эквиваленты между собой с точки зрения синтаксиса.\n", "\n", "Но у второго подхода есть ряд преимуществ, самый главный из которых, наверное, следующий. При первом подходе читатель анализирует тело функции, ещё не зная, что она потом будет задекорирована, т.е. модифицирована. При втором подходе декоратор встречается ещё до ключевого слова `def` и явно указывает на модификацию функции." ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Функция square вызвана с аргументом 2\n", "4\n" ] } ], "source": [ "@announce\n", "def square(x):\n", " return x * x\n", "\n", "print(square(2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Проблема с затиранием имени функции и её решение\n", "\n", "Попробуем получить имя декорированной функции `square` из предыдущего примера." ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'wrap'" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "square.__name__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Упс. Вместо ожидаемого `square` мы получили `wrap`. Это объясняется тем, что \n", "```python\n", "@announce\n", "def square(x):\n", "```\n", "сводится к \n", "```python\n", "square = announce(square)\n", "```\n", "А функция `announce` возвращает обертку `wrap`. Таким образом, имя исходной функции затирается. \n", "\n", "Можно попробовать решить эту проблему задавав необходимое имя функции `wrap` перед её возвращением из функции `announce`. " ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "square\n", ".wrap at 0x00000142CABAF8B0>\n" ] } ], "source": [ "def announce(func):\n", " def wrap(x):\n", " print(f\"Функция {func.__name__} вызвана с аргументом {x}\")\n", " return func(x)\n", " wrap.__name__ = func.__name__\n", " return wrap\n", "\n", "@announce\n", "def square(x):\n", " return x * x\n", "\n", "print(square.__name__)\n", "print(square)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Но куда лучше будет воспользоваться декоратором [wraps](https://docs.python.org/3/library/functools.html#functools.wraps) из модуля [functools](https://docs.python.org/3/library/functools.html), что позволит сохранить все метаданные исходной функции." ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "square\n", "\n" ] } ], "source": [ "from functools import wraps\n", "\n", "def announce(func):\n", " @wraps(func)\n", " def wrap(x):\n", " print(f\"Функция {func.__name__} вызвана с аргументом {x}\")\n", " return func(x)\n", " return wrap\n", "\n", "@announce\n", "def square(x):\n", " return x * x\n", "\n", "print(square.__name__)\n", "print(square)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Декорация функции с произвольной сигнатурой\n", "\n", "В прошлом примере мы наложили ограничение на декорируемую функцию в один аргумент. Это было сделано лишь для наглядности. Используя `*args` и `**kwargs` можно объявлять декораторы для функций с произвольной сигнатурой (см. [](argskwargs)).\n", "\n", "Переопределим декоратор `announce`." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Функция add вызвана с позиционными аргументами (42, 3.14) и именованными аргументами {}.\n", "45.14\n", "Функция add вызвана с позиционными аргументами (7,) и именованными аргументами {'y': 2.71}.\n", "9.71\n" ] } ], "source": [ "def announce(func):\n", " def wrap(*args, **kwargs):\n", " print(f\"Функция {func.__name__} вызвана с позиционными аргументами {args}\", end=\"\")\n", " print(f\" и именованными аргументами {kwargs}.\")\n", " return func(*args, **kwargs)\n", " return wrap\n", "\n", "@announce\n", "def add(x, y):\n", " return x + y\n", "\n", "print(add(42, 3.14))\n", "print(add(7, y=2.71))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Пример: измеряющий время декоратор\n", "\n", "Рассмотрим вариант реализации декоратора, который при вызове декорируемой функции измеряет время, потребовавшееся на её выполнение и выводит его в стандартный поток вывода, прежде чем вернуть результат её вычисления." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Вызов countdown занял 0.0006335000000490254 секунд, (параметры (10000,), {})\n", "Вызов countdown занял 0.006403900000123031 секунд, (параметры (100000,), {})\n", "Вызов countdown занял 0.07007500000008804 секунд, (параметры (1000000,), {})\n", "Вызов countdown занял 0.6575270000000728 секунд, (параметры (10000000,), {})\n" ] } ], "source": [ "from time import perf_counter\n", "import seaborn as sns\n", "\n", "\n", "def timed(func):\n", " def wrap(*args, **kwargs):\n", " t1 = perf_counter()\n", " result = func(*args, **kwargs)\n", " t2 = perf_counter()\n", " print(f\"Вызов {func.__name__} занял {t2- t1} секунд, \", end=\"\")\n", " print(f\"(параметры {args}, {kwargs})\")\n", " return result\n", " return wrap\n", "\n", "\n", "@timed\n", "def countdown(n):\n", " while n > 0:\n", " n -= 1\n", "\n", "\n", "for n in [10_000, 100_000, 1_000_000, 10_000_000]:\n", " countdown(n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Декораторы с аргументами\n", "\n", "На примере декоратора `functools.wraps`, декораторы, принимающие аргументы, объявляются несколько сложнее. Дело в том, что в этом случае необходимо погрузится ещё на один уровень абстракции. Предположим, что вы хотите написать декоратор `my_decorator` с параметрами `x`, `y` и `z`, а применять его в следующем виде:\n", "```python\n", "@my_decorator(x, y, z)\n", "def some_function():\n", " ...\n", "```\n", "Такое объявление будет эквивалентно инструкции\n", "```python\n", "some_function = my_decorator(x, y, z)(some_function)\n", "```\n", "\n", "Иными словами, вызов функции `my_decorator(x, y, z)` должен возвращать другую функцию (или вызываемый объект), которая уже и будет параметризованным декоратором. Т.е. выглядеть она должна приблизительно так:\n", "\n", "```python\n", "def my_decorator(x, y, z):\n", " ...\n", " def decorate(func):\n", " ...\n", " @functools.wraps\n", " def wrap(*args, **kwargs):\n", " ...\n", " result = func(*args, **kwargs)\n", " ...\n", " return result\n", " ...\n", " return wrap\n", " ...\n", " return decorate \n", "```\n", "\n", "Таким образом инструкция \n", "```python\n", "some_function = my_decorator(x, y, z)(some_function)\n", "```\n", "сводится к инструкции\n", "```python\n", "some_function = decorate(some_function)\n", "```\n", "где функция `decorate` --- то, что вернул вызов функции `my_decorator(x, y, z)` --- и является непосредственным декоратором. Функция `decorate` может получить доступ к параметрам `x`, `y` и `z` с которыми была вызвана функция `my_decorator`, т.к. они находятся в пространстве локальных имен внешней для неё функции `my_decorator`. Таким образом поведение функции `decorate` может зависеть от параметров `x`, `y` и `z`.\n", "\n", "В качестве примера, рассмотрим два декоратора. Первый декорирует функцию таким образом, чтобы печаталось заданное сообщение перед вызовом функции, а второй --- после вызова функции." ] }, { "cell_type": "code", "execution_count": 58, "metadata": {}, "outputs": [], "source": [ "from functools import wraps\n", "\n", "def print_before(msg=\"\"):\n", " def decorate(func):\n", " @wraps(func)\n", " def wrap(*args, **kwargs):\n", " print(msg)\n", " return func(*args, **kwargs)\n", " return wrap\n", " return decorate\n", "\n", "def print_after(msg):\n", " def decorate(func):\n", " @wraps(func)\n", " def wrap(*args, **kwargs):\n", " result = func(*args, **kwargs)\n", " print(msg)\n", " return result\n", " return wrap\n", " return decorate" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Приблизительно так может выглядеть реализация таких декораторов. \n", "\n", "Проверим работоспособность первого из них." ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello!\n", "Функция f.\n" ] } ], "source": [ "@print_before(msg=\"Hello!\")\n", "def f():\n", " print(\"Функция f.\")\n", "\n", "f()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим работоспособность второго из них." ] }, { "cell_type": "code", "execution_count": 55, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Функция g.\n", "Good bye!\n" ] } ], "source": [ "@print_after(msg=\"Good bye!\")\n", "def g():\n", " print(\"Функция g.\")\n", "\n", "g()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь убедимся, что мы можем передать декоратору и другое значение, а заодно продемонстрируем возможность цепного применения декораторов. " ] }, { "cell_type": "code", "execution_count": 60, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hi!\n", "Функция h.\n", "Bye!\n" ] } ], "source": [ "@print_before(msg=\"Hi!\")\n", "@print_after(msg=\"Bye!\")\n", "def h():\n", " print(\"Функция h.\")\n", "\n", "h()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{note}\n", "Объявление декораторов с необязательными аргументами и другие примеры можно найти в {cite}`Python.Cookbook`.\n", "```" ] } ], "metadata": { "interpreter": { "hash": "cd49b4596bae9c980ff74fdf93e8fe80e447435ae307c062fad6c4f9ef2eb47f" }, "kernelspec": { "display_name": "Python 3.8.10 ('venv': venv)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.10" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }