metaclass is a useful tool to control the instance creation behavior or applying field validation, enforcing naming convention ,at class creation or object creation time .
in many cases , we could archive the same by using decorator .
#1 __init__ , __new__ and __call__
to start meta class in python , lets start with __new__ __init__ and __call__ methods overrides
class MyMeta(type):
def __new__(cls, name, bases, fields):
print('calling __new__ (#1 allocating memory). only called once ')
return super(MyMeta, cls).__new__(cls, name, bases, fields) def __init__(cls, name, bases, fields):
print('calling __init__(#2 Initialize). only called once')
return super(MyMeta, cls).__init__(name, bases, fields) def __call__(cls, *args, **kwargs):
print('calling __call__ (#3 called when instantiate a object)')
return type.__call__(cls, *args, **kwargs)class X(metaclass=MyMeta):
passcalling __new__ (#1 allocating memory). only called once
calling __init__(#2 Initialize). only called once
calling __call__ (#3 called when instantiate a object)
calling __call__ (#3 called when instantiate a object)
Conclusion — __init__ and __new__ called only once when class created . __call__ being called whenever a new object got created .
#2 Add “meta” fields to class
Now we want to ‘inject’ class_id and object_id field into every class using this metaclass .
import uuid
class ClassMeta(type):
def __new__(cls, name, bases, fields):
cls.class_id = uuid.uuid4().hex
res = super().__new__(cls, name, bases, fields)
return res def __call__(cls, *args, **kwargs):
obj = type.__call__(cls, *args, **kwargs)
obj.unique_id = uuid.uuid4().hex
return objclass X(metaclass=ClassMeta):
passif __name__ == "__main__":
print(X.class_id)
print(X().unique_id)
print(X().unique_id)if __name__ == "__main__":
print(X.unique_id)
print(Y.unique_id)output :
859c5f93f1344a78a7be2bf7a2eacba8
a81f6d47212d43919a9779a8585ecc91
ce88876587fc4619ba9fa0401cfaffa2
__new__ only called when class got created ,could be used to add class level fields ;__call__ could be used to add object level fields ,context is within each individual object
#3 Count how many objects in memory
Sometimes we want to measure application performance .by using metaclass + weakref we could get class_name: <num of objects> to make usage of memory more effetive .
import weakref
class ObjCount(type):
obj_count = {}
def __call__(cls, *args, **kwargs):
print(str(cls))
cls_name = str(cls)
ObjCount.obj_count.setdefault(cls_name, [])
obj = type.__call__(cls, *args, **kwargs)
ObjCount.obj_count[cls_name].append(weakref.ref(obj))
return obj @staticmethod
def stat():
for k,v in ObjCount.obj_count.items():
print(f'class {k} : created {len([x for x in v if x()])} objects')class C(metaclass=ObjCount):
passclass C2(metaclass=ObjCount):
passif __name__ == "__main__":
c1,c2,c3 = C(), C(), C()
c4,c5 = C2(), C2()
ObjCount.stat()
# class <class '__main__.C'> : created 3 objects
# class <class '__main__.C2'> : created 2 objectsdel c1
ObjCount.stat()
#class <class '__main__.C'> : created 2 objects
#class <class '__main__.C2'> : created 2 objects
del c2
del c4
ObjCount.stat()
# class <class '__main__.C'> : created 1 objects
# class <class '__main__.C2'> : created 1 objects
#4 enforce naming convention
meta data class could be useful to enforce (field or method) naming convention ,for example you could enforce field naming to use snake case .
class EnforceSnakeCase(type):
def __new__(meta, names, bases, fields):
attrs = {}
for name, val in fields.items():
attrs[to_snake_case(name)] = val
return type(names, bases, attrs)class X(metaclass=EnforceSnakeCase):
intField=3
strField="abc"def __repr__(self):
return f'{self.int_field} {self.str_field}'if __name__ == "__main__":
x = X()
#print(x.intField) # error
# AttributeError: 'X' object has no attribute 'intField'
x.int_field = 15
x.str_field="dsa"
print(x)
#5 abstract base class
though not a big fan of OOP . but meta could be used to implement abstract class or method .sometimes ,useful to structure your code specially when you building a UI framework or get “abstraction templated”
from abc import ABCMeta, abstractmethod
class UIControl(metaclass=ABCMeta): @abstractmethod
def created(self):
print(f'{type(self)} created') @abstractmethod
def destroy(self):
print(f'{type(self)} destroyed')class Button(UIControl):
passif __name__ == "__main__":
Button()
because base class provided abstract method to enforce child to implement .without doing it will result in below error
error :
Button()
TypeError: Can't instantiate abstract class Button with abstract methods created, destroy
Provide implementation in child class to fix it .
class Button(UIControl):
def created(self):
super().created()
print('button created') def destroy(self):
super().destroy()
print('button destroyted')if __name__ == "__main__":
Button().created()output :
<class '__main__.Button'> created
button created