Python for Everyone!
Oregon Curriculum Network
Note that Python functions, as top-level objects, may be endowed with attributes just like any other object.
That's what we do here: the UFO decorator brands any function with some special mark, of having been abucted by aliens.
Functions may be used to decorate other functions.
In [1]:
def UFO(abductee):
abductee.special_mark = True
return abductee
def subject_A():
pass
@UFO
def subject_B():
pass
print(subject_B.special_mark)
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.
In [2]:
def UFO(attr, value):
"""returns Abduct, poised to proceed"""
def Abduct(abductee): # incoming callable
"""set whatever attribute to the chosen value"""
abductee.__setattr__(attr, value)
return abductee # a callable, remember
return Abduct
@UFO("arm", "special_tattoo") # ">> ☺ <<"
def subject_A():
"""just minding my own busines..."""
pass
class Dog(object):
tricks = ["play dead"]
pass
class Collie(Dog):
pass
print("What's that on Subject A's leg?", subject_A.arm)
dog = Collie()
dog.__setattr__("name", "Lassie")
print(dog.name)
setattr(dog, "favorite", "steak")
print(dog.favorite)
hasattr(dog, "stomach")
print(dog.tricks)
print(dog.__dict__)
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.
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.
Both functions and Composer type objects are directly callable, so expressions like self(x) and other(x) should always make sense.
In [3]:
class Composer:
"""allow function objects to chain together"""
def __init__(self, func):
self.func = func # swallow a function
def __matmul__(self, other):
return Composer(lambda x: self(other(x)))
def __rmatmul__(self, other):
return Composer(lambda x: other(self(x)))
def __call__(self, x):
return self.func(x)
def addA(s):
return s + "A"
def addB(s):
return s + "B"
result = addA(addA(addA("K"))) # ordinary composition
print(result)
result = addB(addA(addB("K")))
print(result)
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 "@".
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.
In [4]:
@Composer
def addA(s):
return s + "A"
def addB(s):
return s + "B"
Chained = addB @ addA @ addB @ addA @ addB # an example of operator overloading
print(Chained("Y"))
Lets write a unittest to make sure even an ordinary, non-decorated function, may be multiplied by a Composer type object...
In [5]:
import unittest
class TestComposer(unittest.TestCase):
def test_composing(self):
def Plus2(x):
return x + 2
@Composer
def Times2(x):
return x * 2
H = Times2 @ Plus2
self.assertEqual(H(10), 24)
def test_composing2(self):
def Plus2(x):
return x + 2
@Composer
def Times2(x):
return x * 2
H = Plus2 @ Times2
self.assertEqual(H(10), 22)
def test_composing3(self):
def Plus2(x):
return x + 2
@Composer
def Times2(x):
return x * 2
H = Plus2 @ Times2
self.assertEqual(H(10), 22)
a = TestComposer() # the test suite
suite = unittest.TestLoader().loadTestsFromModule(a) # fancy boilerplate
unittest.TextTestRunner().run(suite) # run the test suite
Out[5]:
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.
In [6]:
from random import choice
def add_tricks(cls):
tricks = ["play dead", "roll over", "sit up"]
def do_trick(self):
return choice(tricks)
cls.do_trick = do_trick
return cls
@add_tricks
class Animal:
def __init__(self, nm):
self.name = nm
class Mammal(Animal):
pass
obj = Animal("Rover")
print(obj.name, "does this trick:", obj.do_trick())
new_obj = Mammal("Trixy")
print(new_obj.name, "does this trick:", obj.do_trick())
Related reading: