{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "Python for Everyone!
[Oregon Curriculum Network](http://4dsolutions.net/ocn/)\n", "\n", "## Abucted by Aliens\n", "\n", "### Decorators in Python\n", "\n", "![alt txt](http://proofofalien.com/wp-content/uploads/2016/02/10-Tips-Of-How-To-Get-Abducted-By-Aliens-624x257.jpg)\n", "\n", "Note that Python functions, as top-level objects, may be endowed with attributes just like any other object. \n", "\n", "That's what we do here: the UFO decorator brands any function with some special mark, of having been abucted by aliens.\n", "\n", "Functions may be used to decorate other functions." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "True\n" ] } ], "source": [ "def UFO(abductee):\n", " abductee.special_mark = True\n", " return abductee\n", "\n", "def subject_A():\n", " pass\n", "\n", "@UFO\n", "def subject_B():\n", " pass\n", "\n", "print(subject_B.special_mark)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now lets make our decorator take arguments, meaning we'll be able to customize the behavior of what it does. Instead of UFO always setting special_mark to True, we'll allow both the attribute and value to be passed in." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "What's that on Subject A's leg? special_tattoo\n", "Lassie\n", "steak\n", "['play dead']\n", "{'name': 'Lassie', 'favorite': 'steak'}\n" ] } ], "source": [ "def UFO(attr, value):\n", " \"\"\"returns Abduct, poised to proceed\"\"\"\n", " def Abduct(abductee): # incoming callable\n", " \"\"\"set whatever attribute to the chosen value\"\"\"\n", " abductee.__setattr__(attr, value)\n", " return abductee # a callable, remember\n", " return Abduct\n", "\n", "@UFO(\"arm\", \"special_tattoo\") # \">> ☺ <<\"\n", "def subject_A():\n", " \"\"\"just minding my own busines...\"\"\"\n", " pass\n", "\n", "class Dog(object):\n", " tricks = [\"play dead\"]\n", " pass\n", "\n", "class Collie(Dog):\n", " pass\n", "\n", "print(\"What's that on Subject A's leg?\", subject_A.arm)\n", "dog = Collie()\n", "dog.__setattr__(\"name\", \"Lassie\")\n", "print(dog.name)\n", "setattr(dog, \"favorite\", \"steak\")\n", "print(dog.favorite)\n", "hasattr(dog, \"stomach\")\n", "print(dog.tricks)\n", "print(dog.__dict__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Functions do not implement the multiplication method right out of the gate, i.e. if you have two functions, don't expect to compose them into a new function with the multiplication operator, unless and until you have the right Composer class.\n", "\n", "Lets use the @ operator (\\_\\_matmul\\_\\_) instead of \\_\\_mul\\_\\_ (\\*). We need to accept a non-composer on the left i.e. the object implementing the method may be to the right of its argument. That's where \\_\\_rmatmul\\_\\_ comes in. \n", "\n", "Both functions and Composer type objects are directly callable, so expressions like self(x) and other(x) should always make sense." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "KAAA\n", "KBAB\n" ] } ], "source": [ "class Composer:\n", " \"\"\"allow function objects to chain together\"\"\"\n", " \n", " def __init__(self, func):\n", " self.func = func # swallow a function\n", " \n", " def __matmul__(self, other):\n", " return Composer(lambda x: self(other(x)))\n", " \n", " def __rmatmul__(self, other):\n", " return Composer(lambda x: other(self(x)))\n", " \n", " def __call__(self, x):\n", " return self.func(x)\n", "\n", "def addA(s):\n", " return s + \"A\"\n", "\n", "def addB(s):\n", " return s + \"B\"\n", "\n", "result = addA(addA(addA(\"K\"))) # ordinary composition\n", "print(result)\n", "\n", "result = addB(addA(addB(\"K\")))\n", "print(result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So now lets see if we might use Composer as a decorator to turn both functions into Composables, that then multiply together. If so, we may chain them using \"@\".\n", "\n", "Classes may be used to decorate functions. We call these \"class decorators\". Notice as long as one of the two objects is a Composer, the other might still be of the function type." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "YBABAB\n" ] } ], "source": [ "@Composer\n", "def addA(s):\n", " return s + \"A\"\n", "\n", "def addB(s):\n", " return s + \"B\"\n", "\n", "Chained = addB @ addA @ addB @ addA @ addB # an example of operator overloading\n", "print(Chained(\"Y\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Lets write a unittest to make sure even an ordinary, non-decorated function, may be multiplied by a Composer type object..." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "...\n", "----------------------------------------------------------------------\n", "Ran 3 tests in 0.006s\n", "\n", "OK\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import unittest\n", "\n", "class TestComposer(unittest.TestCase):\n", " \n", " def test_composing(self):\n", " \n", " def Plus2(x):\n", " return x + 2\n", " \n", " @Composer\n", " def Times2(x):\n", " return x * 2\n", " \n", " H = Times2 @ Plus2\n", " self.assertEqual(H(10), 24)\n", "\n", " def test_composing2(self):\n", " \n", " def Plus2(x):\n", " return x + 2\n", " \n", " @Composer\n", " def Times2(x):\n", " return x * 2\n", " \n", " H = Plus2 @ Times2\n", " self.assertEqual(H(10), 22)\n", " \n", " def test_composing3(self):\n", " \n", " def Plus2(x):\n", " return x + 2\n", " \n", " @Composer\n", " def Times2(x):\n", " return x * 2\n", " \n", " H = Plus2 @ Times2\n", " self.assertEqual(H(10), 22)\n", " \n", "a = TestComposer() # the test suite\n", "suite = unittest.TestLoader().loadTestsFromModule(a) # fancy boilerplate\n", "unittest.TextTestRunner().run(suite) # run the test suite" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using a decorator function to decorate a class, lets teach an old dog some new tricks. Notice that do_trick, the inject method, retains access to the list of tricks thanks to add_tricks remaining in memory as a closure. " ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Rover does this trick: sit up\n", "Trixy does this trick: sit up\n" ] } ], "source": [ "from random import choice\n", "\n", "def add_tricks(cls):\n", " tricks = [\"play dead\", \"roll over\", \"sit up\"]\n", " def do_trick(self):\n", " return choice(tricks)\n", " cls.do_trick = do_trick\n", " return cls\n", " \n", "@add_tricks\n", "class Animal:\n", " \n", " def __init__(self, nm):\n", " self.name = nm\n", "\n", "class Mammal(Animal):\n", " pass\n", "\n", "obj = Animal(\"Rover\")\n", "print(obj.name, \"does this trick:\", obj.do_trick())\n", "\n", "new_obj = Mammal(\"Trixy\")\n", "print(new_obj.name, \"does this trick:\", obj.do_trick())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Related reading:\n", "\n", "[Descriptors and Properties in Python](https://github.com/4dsolutions/Python5/blob/master/Descriptors%20and%20Properties.ipynb)" ] } ], "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.7" } }, "nbformat": 4, "nbformat_minor": 4 }