{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# Dekoratory funkcji i klas\n", "\n", "## Domknięcie (*closure*)\n", "\n", "Domknięcie, w metodach realizacji języków programowania, jest to obiekt **wiążący funkcję oraz środowisko**, w jakim ta funkcja ma działać.\n", "Środowisko przechowuje wszystkie obiekty wykorzystywane przez funkcję, niedostępne w globalnym zakresie widoczności.\n", "Realizacja domknięcia jest zdeterminowana przez język, jak również przez kompilator.\n", "\n", "Domknięcia występują głównie w językach funkcyjnych, w których funkcje mogą zwracać inne funkcje, wykorzystujące zmienne utworzone lokalnie." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def bind_add(x):\n", " def add(y):\n", " # x jest \"zamknięte\" w definicji\n", " return y + x\n", " return add" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "15" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "add_5 = bind_add(5)\n", "add_5(10)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "667" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "add_665 = bind_add(665)\n", "add_665(2)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Wprowadzenie do dekoratorów\n", "\n", "Dekorator to wzorzec projektowy, pozwalający na dynamiczne dodanie nowej funkcjonalności, w trakcie działania programu.\n", "\n", "W języku Python jest to metoda modyfikacji obiektu wywoływalnego (funkcji, metod klasy, klas) za pomocą domknięć.\n", "\n", "Dekoratory są w Pythonie często spotykaną techniką programistyczną.\n", "Ich zalety to redukcja ilości kodu oraz możliwość kontrolowania funkcji (lub innych obiektów wywoływalnych), w szczególności ich danych wejściowych i zwracanych wartości.\n", "\n", "## Prosty dekorator\n", "\n", "Poniżej przedstawiono implementację dekoratora `@shouter`.\n", "Funkcje udekorowane nim wyświetlają komunikat na początku i pod koniec ich wywołania.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def shouter(func):\n", " def wrapper():\n", " print(\"Before\", func.__name__)\n", " result = func()\n", " print(result)\n", " print(\"After\", func.__name__)\n", " return result\n", " return wrapper" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Można tak zdefiniowanej funkcji użyć do \"nadpisania\" istniejącej już funkcji (tak naprawdę do zmiany tego, na co wskazuje zmienna):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Before greetings\n", "Hi\n", "After greetings\n" ] }, { "data": { "text/plain": [ "'Hi'" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def greetings():\n", " return \"Hi\"\n", "\n", "hello = shouter(greetings)\n", "\n", "hello()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Począwszy od Pythona 2.4, możliwe i rekomendowane jest użycie specjalnej składni:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "@shouter\n", "def hello():\n", " return \"Hello\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Before hello\n", "Hello\n", "After hello\n" ] }, { "data": { "text/plain": [ "'Hello'" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "hello()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Użycie ``@shouter`` przed definicją funkcji jest równoważne umieszczeniu za jej definicją linii ``hello = shouter(hello)``." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Argumenty w dekoratorach\n", "\n", "Problem\n", "*******\n", "\n", "Przedstawiony dekorator działa tylko z funkcjami, które nie przyjmują żadnych argumentów.\n", "Co z funkcjami wymagającymi argumentów?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "@shouter\n", "def add(x, y):\n", " '''Docstring for add(x, y)'''\n", " return x + y" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "wrapper() takes 0 positional arguments but 2 were given", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)\n", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n", "\u001b[0;32m----> 1\u001b[0;31m \u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m7\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0m\n", "\u001b[0;31mTypeError\u001b[0m: wrapper() takes 0 positional arguments but 2 were given" ] } ], "source": [ "add(2, 7)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Innym problemem jest to, że udekorowana funkcja utraciła swój docstring oraz swoją nazwę:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "add.__doc__" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'wrapper'" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "add.__name__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Rozwiązanie\n", "***********\n", "\n", "Argumenty przekazywane do *wrapper* muszą zostać przekazane dalej, do właściwej funkcji *func*.\n", "\n", "Z kolei problem z docstringiem i nazwą rozwiążemy dekorując funkcję *wrapper* przy pomocy dekoratora ``@functools.wraps``, który zadba o skopiowanie docstringa i nazwy:\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import functools\n", "\n", "def shouter(func):\n", " @functools.wraps(func)\n", " def wrapper(*args, **kwargs):\n", " print(\"Before\", func.__name__)\n", " result = func(*args, **kwargs)\n", " print(result)\n", " print(\"After\", func.__name__)\n", " return result\n", " return wrapper" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "@shouter\n", "def add(x, y):\n", " '''Docstring for add(x, y)'''\n", " return x + y" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Before add\n", "11\n", "After add\n" ] }, { "data": { "text/plain": [ "11" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "add(5, 6)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'Docstring for add(x, y)'" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "\n", "add.__doc__" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'add'" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "add.__name__" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Dekoratory parametryzowane\n", "\n", "Dekoratory, które nie przyjmują żadnych argumentów, są często spotykane.\n", "Jednak czasami potrzebujemy przekazać do dekoratora argumenty. \n", "\n", "Aby otrzymać parametryzowany dekorator, musimy go \"owinąć\" w jeszcze jedną funkcję (domknięcie):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def tag(tagname):\n", " def decorator(fun):\n", " @functools.wraps(fun)\n", " def wrapper(*args, **kwargs):\n", " tag_before = f\"<{tagname}>\"\n", " tag_after = f\"\"\n", " fresult= fun(*args, **kwargs) \n", " return tag_before + fresult + tag_after\n", " return wrapper\n", " return decorator" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "@tag(\"b\")\n", "def output(data):\n", " return data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'TEXT'" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "output(\"TEXT\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Użycie ``@tag(\"b\")`` jest odpowiednikiem:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "output = tag(\"b\")(output)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Wiele dekoratorów\n", "\n", "Funkcję można owijać w wiele dekoratorów." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "@shouter\n", "@tag('b')\n", "def my_func(text):\n", " return text" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Before my_func\n", "text\n", "After my_func\n" ] }, { "data": { "text/plain": [ "'text'" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "my_func(\"text\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "co odpowiada:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "my_func = shouter(tag(\"b\")(my_func))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Należy pamiętać, że kolejność ma znaczenie." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Kiedy uruchamiane są dekoratory\n", "\n", "Kluczowe znaczenie dla dekoratorów ma fakt, że są one uruchamiane zaraz po tym jak zdefiniowana została dekorowana funkcja. \n", "Najczęściej jest to moment *importu* pakietu." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "running register()\n", "running register()\n", "running main()\n", "registry -> [, ]\n", "running f1()\n", "running f2()\n", "running f3()\n" ] } ], "source": [ "registry = [] \n", "\n", "def register(func):\n", " print(f'running register({func})') \n", " registry.append(func)\n", " return func\n", "\n", "@register\n", "def f1():\n", " print('running f1()')\n", "\n", "@register\n", "def f2():\n", " print('running f2()')\n", "\n", "def f3():\n", " print('running f3()')\n", "\n", "def main():\n", " print('running main()')\n", " print('registry ->', registry)\n", " f1()\n", " f2()\n", " f3()\n", "\n", "if __name__ == '__main__':\n", " main()" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Dekoratory klas\n", "\n", "Od Pythona 2.6 można dekorować klasy.\n", "W środku dekoratora można zmodyfikować klasę, na przykład zmienić jej metody.\n", "Dekoratory klas mają działanie zbliżone do metaklas.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "id = 0\n", "\n", "def add_id(decorated_class):\n", " original_init = decorated_class.__init__\n", " \n", " def __init__(self, *args, **kwargs):\n", " print(\"add_id init\")\n", " global id\n", " id += 1\n", " self.id = id\n", " original_init(self, *args, **kwargs)\n", " \n", " decorated_class.__init__ = __init__ # replacing __init__ in decorated class\n", " return decorated_class\n", "\n", "@add_id\n", "class Foo(object):\n", " def __init__(self):\n", " print(\"Foo class init\")\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "add_id init\n", "Foo class init\n" ] }, { "data": { "text/plain": [ "1" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "foo = Foo()\n", "foo.id" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "add_id init\n", "Foo class init\n" ] }, { "data": { "text/plain": [ "2" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "bar = Foo()\n", "bar.id" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Klasy jako dekoratory\n", "\n", "Bardzo ciekawym zastosowaniem jest użycie klasy jako dekoratora.\n", "Wystarczy zdefiniować w klasie metodę specjalną `__call__`.\n", "Instancja klasy (uzyskana za pomocą operatora ``()``) staje się wtedy obiektem, który można wywołać.\n", "\n", "Jest to alternatywa dla definiowania nieparametryzowanego dekoratora przy pomocy dwóch zagnieżdżonych funkcji.\n", "Kod jest nieco prostszy do zrozumienia:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class shout:\n", " def __init__(self, f):\n", " print(\"inside decorator's __init__()\")\n", " self.f = f\n", " \n", " def __call__(self):\n", " print(\"before call\")\n", " self.f()\n", " print(\"after call\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "inside decorator's __init__()\n" ] } ], "source": [ "@shout\n", "def function():\n", " print(\"inside function()\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "before call\n", "inside function()\n", "after call\n" ] } ], "source": [ "function()" ] } ], "metadata": { "language_info": { "name": "python" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }