MetaProgramming In Python

Classes in Python - What is a class in Python?


In [ ]:
class Test:
    pass

a = Test()
a

In [ ]:
type(a)

In [ ]:
type(Test)

In [ ]:
type(type)

Classes - Nothing but instances of types. Class technically is a sugar over the native 'type'

What is type in Python?


In [ ]:
type?

In [ ]:
TestWithType = type('TestWithType', (object,), {})

In [ ]:
type(TestWithType)

In [ ]:
ins1 = TestWithType()

In [ ]:
type(ins1)

In [ ]:
type('TestWithType', (object,), {})()

'type' is an important native structure used for creating classes.

Life Cycle involved in a class - Vanilla


In [ ]:
class TestClass:

    def __new__(cls, *args, **kwargs):
        print('new method called')
        instance = super(TestClass, cls).__new__(cls, *args, **kwargs)
        return instance

    def __call__(self, a, b, c):
        self.call_count += 1
        print('call method called')
        return a * b * c

    def __init__(self):
        self.call_count = 0
        super(TestClass, self).__init__()
        print('init method called')
        
    def get_call_count(self):
        return self.call_count

In [ ]:
a = TestClass()

In [ ]:
a(1,2,3)

In [ ]:
a.get_call_count()

What is type? 'type' defines how a class behaves in Python.

Got it. Well then - Can I change 'how' a class behaves in Python? - MetaClasses

Metaclasses


In [ ]:
class MySingletonMeta(type):
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(MySingletonMeta, cls).__call__(*args)
        return cls._instances[cls]

In [ ]:
class MySingletonClass(metaclass=MySingletonMeta):
    def __init__(self):
        self.i = 1

In [ ]:
a = MySingletonClass()
b = MySingletonClass()

In [ ]:
type(a), id(a) , type(b), id(b)

LifeCycle with Metaclasses


In [ ]:
class MyMetaClass(type):
    
    _test_attribute = 1

    def __new__(cls, *args, **kwargs):
        print("metaclass new method called")
        return super(MyMetaClass, cls).__new__(cls, *args, **kwargs)
    
    def __call__(cls, *args, **kwargs):
        print("metaclass call method called")
        return super(MyMetaClass, cls).__call__(*args, **kwargs)

    def __init__(self, *args, **kwargs):
        print("metaclass init method called")
        return super(MyMetaClass, self).__init__(*args, **kwargs)
    
    def test_method_1(self):
        print("MyMetaClass - Test method 1 called")

In [ ]:
class MyClass(metaclass=MyMetaClass):
    def __new__(cls, *args, **kwargs):
        print("instance new method called")
        return super(MyClass, cls).__new__(cls, *args, **kwargs)
    
    def __init__(self, *args, **kwargs):
        print("instance init method called")
        return super(MyClass, self).__init__(*args, **kwargs)

In [ ]:
ins2 = MyClass()

In [ ]:
MyClass._test_attribute

In [ ]:
MyClass.__mro__

In [ ]:
MyMetaClass.__mro__

In [ ]:

Pattern 1 : Abstract Classes


In [ ]:
from abc import ABCMeta, ABC, abstractmethod

In [ ]:
ABCMeta?

In [ ]:
class MyAbstractClass(metaclass=ABCMeta):
    def __init__(self):
        pass

    @abstractmethod
    def my_abstract_method(self):
        pass

In [ ]:
MyAbstractClass()

In [ ]:
class MyChildClass(MyAbstractClass):
    
    def __init__(self):
        pass
    
    def my_abstract_method(self):
        pass

In [ ]:
mcc = MyChildClass()
mcc

Pattern 2 : Abstract family of singleton classes - Combine two metaclasses


In [ ]:
class MySingletonABCMeta(ABCMeta):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(MySingletonABCMeta, cls).__call__(*args)
        return cls._instances[cls]

In [ ]:
class MyAbstractSingletonClass(metaclass=MySingletonABCMeta):
    def __init__(self):
        pass

    @abstractmethod
    def my_abstract_method(self):
        pass

In [ ]:
MyAbstractSingletonClass()

In [ ]:
class MyAbstractSingletonChild(MyAbstractSingletonClass):
    def __init__(self):
        pass
    
    def my_abstract_method(self):
        pass

In [ ]:
a1 = MyAbstractSingletonChild()
b1 = MyAbstractSingletonChild()

In [ ]:
type(a1), id(a1), type(b1), id(b1)

Pattern 3 : Pooled Objects


In [ ]:
class MyBeanMeta(type):
    _instances = {}

    def __call__(cls, *args):
        print(args)
        key = tuple((cls, args))
        if key not in cls._instances:
            cls._instances[key] = super(MyBeanMeta, cls).__call__(*args)
        return cls._instances[key]

In [ ]:
class MyBeanClass(metaclass=MyBeanMeta):
    def __init__(self, a ):
        self.a = a

In [ ]:
bn1 = MyBeanClass(1)
bn2 = MyBeanClass(2)
bn3 = MyBeanClass(3)
bn4 = MyBeanClass(1)

In [ ]:
id(bn1), id(bn2), id(bn3), id(bn4)

Pattern 4 : Logging using Metaclasses


In [1]:
import logging

logging.basicConfig(filename='example.log', level=logging.INFO)
logging.debug('This message should go to the log file')
logging.info('So should this')
logging.warning('And this, too')


class MyLogSingletonMeta(type):
    logger = logging.getLogger('abc')

    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super(MyLogSingletonMeta, cls).__call__(*args)
            cls._instances[cls] = instance

            instance.__dict__['logger'] = logging.getLogger('abc')
        return cls._instances[cls]


class MyLogEnabledClass(metaclass=MyLogSingletonMeta):
    def test_function(self):
        self.logger.info('Inside test_function method of Log Enabled class')
        pass

In [2]:
lec_instance1 = MyLogEnabledClass()
lec_instance2 = MyLogEnabledClass()
lec_instance1.test_function()

print(id(lec_instance1), id(lec_instance2))


4453879696 4453879696

In [3]:
!cat example.log


INFO:root:So should this
WARNING:root:And this, too
INFO:abc:Inside test_function method of Log Enabled class

In [4]:
class MyLogger:    
    def __init__(self, logger=None):
        self.logger = logger
        
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            if self.logger is None:
                print(str(func) + " is called")
            else:
                self.logger.info(str(func) + " is called")
            return func(*args, **kwargs)
        return wrapper 

class MyLoggingMeta(type):
    
    def __new__(cls, name, bases, attrs):        
        for item, value in attrs.items():
            if callable(value):
                print("Function item :" + str(item), str(value), type(value))
                attrs[item] = MyLogger()(value)
            else: 
                print(str(item), str(value), type(value))
        return super(MyLoggingMeta, cls).__new__(cls, name, bases, attrs)

In [5]:
class MyClass1(metaclass=MyLoggingMeta):
    def test_m1(self):
        pass
    
    def test_m2(self):
        pass


__module__ __main__ <class 'str'>
__qualname__ MyClass1 <class 'str'>
Function item :test_m1 <function MyClass1.test_m1 at 0x109798560> <class 'function'>
Function item :test_m2 <function MyClass1.test_m2 at 0x1097985f0> <class 'function'>

In [6]:
a= MyClass1()

In [7]:
a.test_m2()


<function MyClass1.test_m2 at 0x1097985f0> is called

In [8]:
a.test_m1()


<function MyClass1.test_m1 at 0x109798560> is called

Pattern 5 : Sealed classes


In [ ]:
class MySealedMeta(type):
    
    def __new__(cls, name, bases, attrs):
        all_metaclasses = [type(x) for x in bases]
        if MySealedMeta in all_metaclasses:
            raise TypeError("Sealed class cannot be sublcassed")
        return super(MySealedMeta, cls).__new__(cls, name, bases, attrs)

In [ ]:
class MySealedClass(metaclass=MySealedMeta):
    pass

In [ ]:
class MyChildOfSealed(MySealedClass):
    pass

In [ ]: