Explore Python Function Decorators

I finished reading this book on Python function decorators.

Time to explore. I have used a lot of function decorators.

I have never written my own.

In [1]:
import logging

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

In [2]:
def verbose(func):

def wrapper():
logger.debug(f'{verbose.__name__} before: "{func.__name__}"')
result = func()
logger.debug(f'{verbose.__name__} after: "{func.__name__}"')
return result

return wrapper

In [3]:
def hello_world():
print('Hello, world.')

In [4]:
hello_world()

Hello, world.

In [5]:
hello_world = verbose(hello_world)
type(hello_world)

Out[5]:
function
In [6]:
hello_world()

DEBUG:__main__:verbose before: "hello_world"
DEBUG:__main__:verbose after: "hello_world"

Hello, world.


Add an accumulator to a Fiboncacci generator.¶

Module: itertools The module itertools is a collection of very powerful—and carefully designed—functions for performing iterator algebra. That is, these allow you to combine iterators in sophisticated ways without having to concretely instantiate anything more than is currently required. …we might simply create a single lazy iterator to generate both the current number and this sum

Excerpt From: David Mertz. “Function Programming in Python.” iBooks.

In [7]:
from itertools import accumulate, tee
from collections import namedtuple

def include_accumulator(func):

def wraps():
t, s = tee(func())
return zip(t, accumulate(s))
return wraps

In [8]:
@include_accumulator
def fibonacci():
a, b = 1, 1
while True:
yield a
a, b = b, a + b

In [9]:
Result = namedtuple('Result', 'fib total')
for _, (fib, total) in zip(range(7), fibonacci()):
print(Result(fib, total))

Result(fib=1, total=1)
Result(fib=1, total=2)
Result(fib=2, total=4)
Result(fib=3, total=7)
Result(fib=5, total=12)
Result(fib=8, total=20)
Result(fib=13, total=33)


Use a class to define the decorator.¶

Most any callable will likely work as expected.

In [10]:
class CallCounter:
def __init__(self, func):
for message in (
f'"{self}" initialization.',
repr(func),
):
logger.debug(message)
self.count = 0
self.func = func

def __call__(self):
self.count += 1
return self.func()

In [11]:
@CallCounter
def hello_world():
return 'Hello, world.'

DEBUG:__main__:"<__main__.CallCounter object at 0x7f4fcffe1870>" initialization.
DEBUG:__main__:<function hello_world at 0x7f4fe48562d0>

In [12]:
hello_world

Out[12]:
<__main__.CallCounter at 0x7f4fcffe1870>
In [13]:
Result = namedtuple('Result', 'output count')

for _ in range(10):
print(Result(hello_world(), hello_world.count))

Result(output='Hello, world.', count=1)
Result(output='Hello, world.', count=2)
Result(output='Hello, world.', count=3)
Result(output='Hello, world.', count=4)
Result(output='Hello, world.', count=5)
Result(output='Hello, world.', count=6)
Result(output='Hello, world.', count=7)
Result(output='Hello, world.', count=8)
Result(output='Hello, world.', count=9)
Result(output='Hello, world.', count=10)


Accept arguments in decorator.¶

This is a contrived example just to explore how decorators work.

In [14]:
class apply:

def __init__(self, f=None, *args, **kwargs):
logger.debug('__init__ called')
for item in (args, kwargs):
logger.debug(repr(item))
self.f = f

def __call__(self, *args, **kwargs):
logger.debug('__call__')
for item in (self.f, args, kwargs):
logger.debug(repr(item))
func, = args

def f():
return self.f(func())
return f

@apply(lambda x: x.split())
def hello_world():
return 'Hello, world.'

hello_world()

DEBUG:__main__:__init__ called
DEBUG:__main__:()
DEBUG:__main__:{}
DEBUG:__main__:__call__
DEBUG:__main__:<function <lambda> at 0x7f4fcffdbc30>
DEBUG:__main__:(<function hello_world at 0x7f4fe4856230>,)
DEBUG:__main__:{}

Out[14]:
['Hello,', 'world.']