Python Metaprogramming Quick Reference

Table Of Contents

  1. Decorator Implementation
  2. Advanced Decorators
  3. Metaclasses

1. Decorator Implementation

At their heart, decorators are functions that take a function as input and return a wrapping function as output


In [3]:
def hello_world():
    print('hello world')
    
# wrap hello world in a function that does logging
def wrap_hello():
    print('Enter: hello_world')
    hello_world()
    print('Exit: hello_world')
    
wrap_hello()


Enter: hello_world
hello world
Exit: hello_world

In [4]:
# to wrap any function at all, write a generic wrapper that takes the a function as input
def logthis(func):
    print('Enter: {}'.format(func.__name__))
    func()
    print('Exit: {}'.format(func.__name__))
    
logthis(hello_world)


Enter: hello_world
hello world
Exit: hello_world

In [5]:
# add the ability to handle arbitrary parameters and return types
def logthis(func):
    def wrapper(*args, **kwargs):
        print('Enter: {}'.format(func.__name__))
        result = func(*args, **kwargs)
        print('Exit: {}'.format(func.__name__))
        return result
    return wrapper  # return the inner function

logged_hello = logthis(hello_world)
logged_hello()


Enter: hello_world
hello world
Exit: hello_world

In [6]:
# now you can replace hello_world with the wrapped function
hello_world=logged_hello
hello_world()


Enter: hello_world
hello world
Exit: hello_world

In [7]:
# the only problem is that function metadata is wrong
hello_world.__name__


Out[7]:
'wrapper'

In [8]:
#instead use functools.wraps and a decorator on your function, which otherwise is equivalent
from functools import wraps

def logthis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('Enter: {}'.format(func.__name__))
        result = func(*args, **kwargs)
        print('Exit: {}'.format(func.__name__))
        return result
    return wrapper  # return the inner function

hello_world=None

@logthis
def hello_world():
    print('hello world')
    
hello_world()


Enter: hello_world
hello world
Exit: hello_world

In [9]:
#if you use wraps, you can also access the raw undecorated function using __wrapped__

raw_hello = hello_world.__wrapped__
raw_hello()


hello world

2. Advanced Decorators


In [11]:
# Decorator that takes arguments
def logged(prefix):
    prefix = prefix if prefix else ''
    logprefix = prefix + ':' if len(prefix) > 0 else prefix
    def decorate(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print('{}Enter:{}'.format(logprefix, func.__name__))
            result = func(*args, **kwargs)
            print('{}Exit:{}'.format(logprefix, func.__name__))
            return result
        return wrapper
    return decorate

@logged('logger')
def hello_world():
    print('hello world')

hello_world()


logger:Enter:hello_world
hello world
logger:Exit:hello_world

In [13]:
# always declare classmethod and staticmethod decorators first (so they are applied last)

class Spam:
    @logthis
    def instance_method(self, n):
        print(self, n)
        while n > 0:
            n -= 1

    @classmethod
    @logthis
    def class_method(cls, n):
        print(cls, n)
        while n > 0:
            n -= 1

    @staticmethod
    @logthis
    def static_method(n):
        print(n)
        while n > 0:
            n-=1

s = Spam()
s.instance_method(4)


Enter: instance_method
<__main__.Spam object at 0x0000020047229CF8> 4
Exit: instance_method

In [14]:
Spam.class_method(10)


Enter: class_method
<class '__main__.Spam'> 10
Exit: class_method

3. Metaclasses


In [16]:
# Singleton metaclass

class Singleton(type):
    def __init__(self, *args, **kwargs):
        self.__instance = None
        super().__init__(*args, **kwargs)
    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            self.__instance = super().__call__(*args, **kwargs)
            return self.__instance
        else:
            return self.__instance

# Example
class Spam(metaclass=Singleton):
    def __init__(self):
        print('Creating Spam')

a = Spam()   #generates 'Creating Spam'
b=Spam()     #no print message produced
b is a


Creating Spam
Out[16]:
True

In [17]:
# Metaclass to cache objects by string provided in ctor

import weakref

class Cached(type):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__cache = weakref.WeakValueDictionary()
    def __call__(self, *args):
        if args in self.__cache:
            return self.__cache[args]
        else:
            obj = super().__call__(*args)
            self.__cache[args] = obj
            return obj

# Example
class Spam(metaclass=Cached):
    def __init__(self, name):
        print('Creating Spam({!r})'.format(name))
        self.name = name
        
a =Spam('a')
b=Spam('b')
b2 = Spam('b')
a is b


Creating Spam('a')
Creating Spam('b')
Out[17]:
False

In [18]:
b is b2


Out[18]:
True