Python for Everyone!
Oregon Curriculum Network

Abucted by Aliens

Decorators in Python

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)


True

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__)


What's that on Subject A's leg? special_tattoo
Lassie
steak
['play dead']
{'name': 'Lassie', 'favorite': 'steak'}

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)


KAAA
KBAB

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"))


YBABAB

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


...
----------------------------------------------------------------------
Ran 3 tests in 0.006s

OK
Out[5]:
<unittest.runner.TextTestResult run=3 errors=0 failures=0>

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())


Rover does this trick: sit up
Trixy does this trick: sit up