STACKING DECORATORS

Lets look at decorators again. They're related to what we call "function composition" in that the decorator "eats" what's defined just below it, and returns a proxy with the same name, likewise a callable.

Here's a decorator that tells the identity function under it what letter to tack on (concatenate) to the end of string s, the argument.

The decorator itself takes an argument. The callable it then returns, the adder function, is ready to do the work of finally "eating" ident and extending what it does by the one letter).


In [1]:
def plus(char):
    """
    returns a prepped adder to eat the target, and to
    build a little lambda that does the job.
    """
    def adder(f):
        return lambda s: f(s) + char
    return adder

@plus('R')     
def ident(s):
    return s

ident('X')  # do the job!


Out[1]:
'XR'

Now let's stack up some decorators, showing how each swallows the result below. Work from the bottom up...


In [2]:
# append from the bottom up, successive wrappings
@plus('W')
@plus('A')
@plus('R')
def ident(s):
    return s

print("ident('X')  :", ident('X'))
print("ident('WAR'):", ident('WAR'))


ident('X')  : XRAW
ident('WAR'): WARRAW

Now let's bring Compose into the mix, a decorator class makes our proxies able to compose with one another by means of multiplication. Even powering has been implemented. We're free to make our target functions composable, in addition to controlling what letters to add.


In [3]:
class Compose:
    """
    make function composible with multiply
    also make self powerable e.g. f ** 3 == f * f * f
    From: https://repl.it/@kurner
    """

    def __init__(self, f):
        self.func = f

    def __mul__(self, other):
        return Compose(lambda x: self(other(x)))

    def __pow__(self, n):
        if n == 0:
            return Compose(lambda x: x) # identity function
        if n == 1:
            return self
        if n > 1:
            me = self
            for _ in range(n-1):
                me *= self # me times me times me...
            return me
    
    def __call__(self, x): # callable instances
        return self.func(x)

In [4]:
@Compose
@plus('W')
@plus('A')
@plus('R')
def ident(s):
    return s

H = ident ** 3
H('X')


Out[4]:
'XRAWRAWRAW'

In [5]:
@Compose
@plus('T')
@plus('Y')
@plus('P')
def F(s):
    return s

@Compose
@plus('N')
@plus('O')
@plus('H')
def G(s):
    return s

H = F * G * G * F
H('')


Out[5]:
'PYTHONHONPYT'

In [6]:
@plus('EXPERIMENT!')
@plus('ANOTHER ')
@plus('DO ')
@plus('LETS ')
def ident(s): return s

ident('')


Out[6]:
'LETS DO ANOTHER EXPERIMENT!'