{
"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
}