{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Decorator unter Python\n", "\n", "Dekoratoren sind ein fortgeschrittenes Konzept in Python. Sie kommen an vielen unterschiedlichen Stellen zum Einsatz.\n", "\n", "\n", " @app.route(\"/home\")\n", " def home():\n", " return render_template(\"index.html\")\n", "\n", " @performance_analysis\n", " def foo():\n", " pass\n", "\n", " @property\n", " def total_requests(self):\n", " return self._total_requests\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Wir können sie am besten verstehen, indem wir anschauen, was in Python mit Methoden alles möglich ist." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "def get_hello_function(punctuation):\n", " \"\"\"Returns a hello world function, with or without punctuation.\"\"\"\n", "\n", " def hello_world():\n", " print(\"Hello world\")\n", "\n", " def hello_world_punctuated():\n", " print(\"Hello, world!\")\n", "\n", " if punctuation:\n", " return hello_world_punctuated\n", " else:\n", " return hello_world" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Die Funktion `get_hello_function` gibt eine Funktion zurück. Je nachdem, welchen Wert der Parameter `punctuation` hat." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello, world!\n" ] } ], "source": [ "ready_to_call = get_hello_function(punctuation=True)\n", "\n", "ready_to_call()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Die Methode, die wir bei dem Aufruf von `get_hello_function` erhalten, wird in einer Variablen gespeichert und kann dann aufgerufen werden. Wir testen es auch mit der anderen Variante:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello world\n" ] } ], "source": [ "ready_to_call = get_hello_function(punctuation=False)\n", "\n", "ready_to_call()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nun schreiben wir eine Funktion, die eine weitere Funktion als Parameter besitzt. Diese Funktion wird in eine andere Funktion eingewickelt und dann als Ergebnis zurückgegeben. Durch das Einwickeln wird in diesem Beispiel der Aufruf um einige Sekunden verzögert, bevor die Funktion aufgerufen wird." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "from time import sleep\n", "\n", "def delayed_func(func):\n", " \"\"\"Return a wrapper which delays `func` by some seconds.\"\"\"\n", " def wrapper():\n", " print(\"Waiting for some seconds...\")\n", " sleep(3)\n", " # Call the function that was passed in\n", " func()\n", "\n", " return wrapper\n", "\n", "\n", "def print_phrase(): \n", " print(\"Fresh Hacks Every Day\")" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Waiting for some seconds...\n", "Fresh Hacks Every Day\n" ] } ], "source": [ "delayed_print_function = delayed_func(print_phrase)\n", "delayed_print_function()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Das sieht irgendwie verwirrend aus. Wichtig ist, dass wir die Funktionalität von `func` selbst nicht verändert haben. Sie wurde um eine weitere Funktionalität ergänzt - mit dieser *dekoriert*." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Übung\n", "\n", "Probiere doch einmal selbst, einen Funktion zu schreiben, die eine andere Funktion dekoriert.\n", "Das folgende kleine Beispiel kann hierbei als Anregung dienen.\n", "\n", "Erstelle eine Funktion `double_call`, die eine Funktion f als Parameter erhält und eine neue Funktion zurückgibt, welche f zweimal hintereinander aufruft.\n", "\n", "```python\n", "def double_call(f):\n", " ...\n", "\n", "def output():\n", " print('output')\n", "\n", "print('double:')\n", "double_output = double_call(output)\n", "double_output() # -> output output\n", "print('quad:')\n", "quad_output = double_call(double_output)\n", "quad_output() # -> output output output output \n", "````\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Die @-Notation\n", "\n", "Im nächsten Schritt verschönern wir die Dekoration etwas. Python hat für das Einwickeln eine spezielle Notation mit einem @-Zeichen." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "from time import sleep\n", "\n", "def delayed_func(func):\n", " \"\"\"Return `func`, delayed by 10 seconds.\"\"\"\n", " def wrapper():\n", " print(\"Waiting for some seconds...\")\n", " sleep(3)\n", " func()\n", " \n", " return wrapper\n", "\n", "@delayed_func\n", "def print_phrase():\n", " print(\"Fresh Hacks Every Day\")" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Waiting for some seconds...\n", "Fresh Hacks Every Day\n" ] } ], "source": [ "print_phrase()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Durch die Annotation von `print_phrase` mit `@delayed_func` wird die Funktion `print_phrase` durch eine andere Funktion ersetzt, die eine verzögerte Ausführung bewirkt." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Nützlich?!\n", "\n", "Warum ist das nützlich? Dekoratoren können Funktionen nicht in ihrem Verhalten verändern; aber sie können sie um neues Verhalten erweitern. Hierfür gibt es viele verschiedene Anwendungen - z.B. für das Debugging.\n", "\n", "Schreiben wir nun einen Dekorator, der eine Zeitmessung durchführt." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "import datetime\n", "import time\n", "\n", "def log_performance(func):\n", " def wrapper():\n", " now = datetime.datetime.now()\n", " print(\"Function called at \" + str(now))\n", " start = time.time()\n", " func()\n", " delta = time.time() - start\n", " print(\"Execution took \" + str(delta) + \" seconds\")\n", " \n", " return wrapper" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Mit dem neuen Dekorator `log_performance` können Funktionsaufrufe nun gemessen werden." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Function called at 2018-09-02 16:44:03.682970\n", "Execution took 2.4941389560699463 seconds\n" ] } ], "source": [ "@log_performance\n", "def calculate_squares():\n", " for i in range(10000000):\n", " i_sq = i**2\n", " \n", "calculate_squares()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parameter\n", "\n", "Die bisherigen Beispiele haben Funktionen ohne Parameter verwendet. Der Dekorator `log_performance` soll aber mit beliebigen Funktionen umgehen können - also auch mit solchen, die Parameter entgegennehmen." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "import datetime\n", "import time\n", "\n", "def log_performance(func):\n", " def wrapper(*args, **kwargs):\n", " now = datetime.datetime.now()\n", " print(\"Function called at \" + str(now))\n", " start = time.time()\n", " result = func(*args, **kwargs)\n", " delta = time.time() - start\n", " print(\"Execution took \" + str(delta) + \" seconds\")\n", "\n", " return result\n", " \n", " return wrapper" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Der wrapper wird nun mit einer Liste `args` oder einem Dictionary `kwargs` aufgerufen - `kw` steht hierbei für *keyword*." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Function called at 2018-09-02 16:44:06.205656\n", "Execution took 2.4768545627593994 seconds\n" ] } ], "source": [ "@log_performance\n", "def calculate_squares(n):\n", " \"\"\"Calculate the squares of the numbers 0 to n.\"\"\"\n", " for i in range(n):\n", " i_squared = i**2\n", "\n", "calculate_squares(10000000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Validierung\n", "\n", "Eine weitere sinnvolle Anwendung von Dekoratoren kann die Validierung von Rückgabewerten sein - z.B. von Ports für Netzwerkkonfigurationen." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "def get_server_addr():\n", " \"\"\"Return IP address and port of server.\"\"\"\n", " ...\n", " return ('192.168.1.0', 8080)\n", "\n", "def get_proxy_addr():\n", " \"\"\"Return IP address and port of proxy.\"\"\"\n", " ...\n", " return ('127.0.0.1', 12253)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('192.168.1.0', 8080)" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "get_server_addr()" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('127.0.0.1', 12253)" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "get_proxy_addr()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Die Validierung der Rückgabewerte dieser beiden Funktionen soll durch den folgenden Dekorator `validate_port` geprüft werden." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "PORTS_IN_USE = [1500, 1834, 7777, 8080]\n", "\n", "def validate_port(func):\n", " def wrapper(*args, **kwargs):\n", " # Call `func` and store the result\n", " result = func(*args, **kwargs)\n", " ip_addr, port = result\n", "\n", " if port < 1024:\n", " raise ValueError(\"Cannot use priviledged ports below 1024\")\n", " elif port in PORTS_IN_USE:\n", " raise RuntimeError(\"Port \" + str(port) + \" is already in use\")\n", "\n", " # If there were no errors, return the result\n", " return result\n", " return wrapper" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nun können die beiden oben definierten Funktion mit dem neuen Dekorator versehen werden." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "@validate_port\n", "def get_server_addr():\n", " \"\"\"Return IP address and port of server.\"\"\"\n", " ...\n", " return ('192.168.1.0', 8080)\n", "\n", "@validate_port\n", "def get_proxy_addr():\n", " \"\"\"Return IP address and port of proxy.\"\"\"\n", " ...\n", " return ('127.0.0.1', 12253)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Der Vorteil bei diesem Vorgehen: Die Validierung wird außerhalb der Kernfunktionalität der Netzwerkfunktionen durchgeführt." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Error: Port 8080 is already in use\n" ] } ], "source": [ "try:\n", " get_server_addr()\n", "except RuntimeError as ve:\n", " print(\"Error:\", ve)" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('127.0.0.1', 12253)" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "get_proxy_addr()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Dokumentation von Funktionen\n", "\n", "Angenommen, wir wollen Metdaten zu unseren Funktionen abrufen - z.B. den Name oder die Dokumentation." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'wrapper'" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "get_server_addr.__name__" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "get_server_addr.__doc__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Der Name der Funktion hat sich geändert und der Dokumentationsstring ist verschwunden. Das ist nicht, was wir erwarten. Es ist aber verständlich, da durch den Dekorator die Funktion ausgetauscht wurd. Leider wurden hierbei die Metadaten nicht ersetzt." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Gott sei Dank wurde dieses Problem schon im Modul `functools` gelöst. Hier gibt es eine Dekorator `wraps`, der alle Metadaten ergänzt." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "from functools import wraps\n", "\n", "def validate_port(func):\n", " @wraps(func)\n", " def wrapper(*args, **kwargs):\n", " result = func(*args, **kwargs)\n", " ip_addr, port = result\n", " \n", " if port < 1024:\n", " raise ValueError(\"Cannot use priviledged ports below 1024\")\n", " elif port in PORTS_IN_USE:\n", " raise RuntimeError(\"Port \" + str(port) + \" is already in use\")\n", "\n", " # If there were no errors, return the result\n", " return result\n", " \n", " return wrapper" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Mit diesem neuen Dekorator können wir die Methoden erneut dekorieren. Die Metadaten bleiben nun erhalten." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "@validate_port\n", "def get_server_addr():\n", " \"\"\"Return IP address and port of server.\"\"\"\n", " ...\n", " return ('192.168.1.0', 8080)\n", "\n", "@validate_port\n", "def get_proxy_addr():\n", " \"\"\"Return IP address and port of proxy.\"\"\"\n", " ...\n", " return ('127.0.0.1', 12253)" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'get_server_addr'" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "get_server_addr.__name__" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'Return IP address and port of server.'" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "get_server_addr.__doc__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Quelle\n", "\n", "Dieses Tutorial basiert auf einem [Artikel bei hackaday](https://hackaday.com/2018/08/31/an-introduction-to-decorators-in-python/)." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.7.0" } }, "nbformat": 4, "nbformat_minor": 2 }