Metaprogramming is a technique of writing computer programs that can treat themselves as data, so you can introspect, generate, and/or modify them while running
Well, usually you don't.
Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why). Tim Peters
However:
The potential uses for metaclasses are boundless. Some ideas that have been explored include logging, interface checking, automatic delegation, automatic property creation, proxies, frameworks, and automatic resource locking/synchronization. Python Documentation
A typical use for a metaclass is creating an API (e.g. the Django ORM). It allows us to define something like this:
class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
But if we do this:
guy = Person(name='bob', age=35)
print(guy.age)
IntegerField
object. It will return an int
, and can even take it directly from the database.models.Model
uses the ModelBase
metaclass and it uses some magic that will turn the Person
you just defined with simple statements into a complex hook to a database field.__init__
method, I am able to instantiate a Person
object. This is done through the metaclass.@abstractmethod
and @abstractproperty
to define abstract methods and properties.issubclass
and isinstance
will work.from abc import ABCMeta, abstractmethod
class Vehicle(metaclass=ABCMeta):
@abstractmethod
def change_gear(self):
pass
@abstractmethod
def start_engine(self):
pass
class Car(Vehicle): # subclass the ABC, abstract methods MUST be overridden
def __init__(self, make, model, color):
self.make = make
self.model = model
self.color = color
try:
car = Car('Toyota', 'Avensis', 'silver')
except TypeError as e:
print(e)
Can't instantiate abstract class Car with abstract methods change_gear, start_engine
but but but... I want to make my own metaclasses!
camelCase
for all their methods instead of underscore_method_names
.get_first_name
and hate getFirstName
__getattr__
:__getattr__
. Problems with inherited methods.inspect.getmembers(MyClass, inspect.isfunction)
and rename functions. This works but:class ObjectCreator:
pass
print(ObjectCreator) # you can pass a class as a parameter because it's an object
<class '__main__.ObjectCreator'>
print(hasattr(ObjectCreator, 'new_attribute'))
ObjectCreator.new_attribute = 'foo' # you can add attributes to a class
print(hasattr(ObjectCreator, 'new_attribute'))
print(ObjectCreator.new_attribute)
False True foo
ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
print(ObjectCreatorMirror.new_attribute)
print(ObjectCreatorMirror())
foo <__main__.ObjectCreator object at 0x00000169EECB63C8>
type
? The good old function that lets you know what type an object is.type
can also create classes on the fly. type
can take the description of a class as parameters, and return a class as type(name, bases, attrs)
.name
is a string giving the name of the class to be constructed.bases
is a tuple giving the parent classes of the class to be constructed.attrs
is a dictionary of the attributes and methods of the class to be constructed.For example:
class Foo:
bar = True
is equivalent with:
Foo = type('Foo', (), {'bar':True})
MyClass = MetaClass()
, MyObject = MyClass()
type
.()
.MyClass()
calls the __init__()
function of the class__call__()
. foo()
is equivalent to foo.__call__()
. This is true in both cases (i.e. all functions objects implement __call__
).class A:
...
you are actually not creating the class, you describe the class and type
creates this class object for you.
import re
def convert(name):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
# the metaclass will automatically get passed the same arguments that we pass to `type`
def camel_to_snake_case(name, bases, attrs):
"""Return a class object, with its attributes from camelCase to snake_case."""
print("Calling the metaclass camel_to_snake_case to construct class: {}".format(name))
# pick up any attribute that doesn't start with '__' and snakecase it
snake_attrs = {}
for attr_name, attr_val in attrs.items():
if not attr_name.startswith('__'):
snake_attrs[convert(attr_name)] = attr_val
else:
snake_attrs[attr_name] = attr_val
return type(name, bases, snake_attrs) # let `type` do the class creation
class MyVector(metaclass=camel_to_snake_case):
def addToVector(self, other): pass
def subtractFromVector(self, other): pass
def calculateDotProduct(self, other): pass
def calculateCrossProduct(self, other): pass
def calculateTripleProduct(self, other): pass
print([a for a in dir(MyVector) if not a.startswith('__')])
Calling the metaclass camel_to_snake_case to construct class: MyVector ['add_to_vector', 'calculate_cross_product', 'calculate_dot_product', 'calculate_triple_product', 'subtract_from_vector']
__new__
and __init__
¶def meta_function(name, bases, attrs):
print('Calling meta_function')
return type(name, bases, attrs)
class MyClass1(metaclass=meta_function):
def __new__(cls, *args, **kwargs):
"""
Called to create a new instance of class `cls`. __new__ takes the class
of which an instance was requested as its first argument. The remaining
arguments are those passed to the object constructor expression
(the call to the class). The return value of __new__ should be the
new object instance (usually an instance of cls).
"""
print('MyClass1.__new__({}, *{}, **{})'.format(cls, args, kwargs))
return super().__new__(cls)
def __init__(self, *args, **kwargs):
"""
Called after the instance has been created (by __new__), but before it
is returned to the caller. The arguments are those passed to the object
constructor. Note: both __new__ and __init__ receive the same arguments.
"""
print('MyClass1.__init__({}, *{}, **{})'.format(self, args, kwargs))
Calling meta_function
a = MyClass1(1, 2, 3, x='ex', y='why')
MyClass1.__new__(<class '__main__.MyClass1'>, *(1, 2, 3), **{'x': 'ex', 'y': 'why'}) MyClass1.__init__(<__main__.MyClass1 object at 0x00000169EED619E8>, *(1, 2, 3), **{'x': 'ex', 'y': 'why'})
__prepare__
, __new__
, __init__
, __call__
¶class MyMeta(type):
@classmethod
def __prepare__(mcs, name, bases, **kwargs):
"""
Called before the class body is executed and it must return a dictionary-like object
that's used as the local namespace for all the code from the class body.
"""
print("Meta.__prepare__(mcs={}, name={}, bases={}, **{}".format(
mcs, name, bases, kwargs))
return {}
def __new__(mcs, name, bases, attrs, **kwargs):
"""
Like __new__ in regular classes, which returns an instance object of the class
__new__ in metaclasses returns a class object, i.e. an instance of the metaclass
"""
print("MyMeta.__new__(mcs={}, name={}, bases={}, attrs={}, **{}".format(
mcs, name, bases, list(attrs.keys()), kwargs))
return super().__new__(mcs, name, bases, attrs)
def __init__(cls, name, bases, attrs, **kwargs):
"""
Like __init__ in regular classes, which initializes the instance object of the class
__init__ in metaclasses initializes the class object, i.e. the instance of the metaclass
"""
print("MyMeta.__init__(cls={}, name={}, bases={}, attrs={}, **{}".format(
cls, name, bases, list(attrs.keys()), kwargs))
super().__init__(name, bases, attrs)
# Note: all three above methods receive as arguments:
# 1. The name, bases and attrs of the future class that will be created
# 2. Keyword arguments passed in the class inheritance list
def __call__(cls, *args, **kwargs):
"""
This is called when we make an instance of the class constructed with the metaclass
"""
print("MyMeta.__call__(cls={}, args={}, kwargs={}".format(cls, args, kwargs))
self = super().__call__(*args, **kwargs)
print("MyMeta.__call__ return: ", self)
return (self)
print("Metaclass MyMeta created")
Metaclass MyMeta created
class MyClass2(metaclass=MyMeta, extra=1):
def __new__(cls, s, a=0, b=0):
print("MyClass2.__new__(cls={}, s={}, a={}, b={})".format(cls, s, a, b))
return super().__new__(cls)
def __init__(self, s, a=0, b=0):
print("MyClass2.__init__(self={}, s={}, a={}, b={})".format(self, s, a, b))
self.a, self.b = a, b
print("Class MyClass created")
Meta.__prepare__(mcs=<class '__main__.MyMeta'>, name=MyClass2, bases=(), **{'extra': 1} MyMeta.__new__(mcs=<class '__main__.MyMeta'>, name=MyClass2, bases=(), attrs=['__module__', '__qualname__', '__new__', '__init__', '__classcell__'], **{'extra': 1} MyMeta.__init__(cls=<class '__main__.MyClass2'>, name=MyClass2, bases=(), attrs=['__module__', '__qualname__', '__new__', '__init__', '__classcell__'], **{'extra': 1} Class MyClass created
a = MyClass2('hello', a=1, b=2)
print("MyClass instance created: ", a)
MyMeta.__call__(cls=<class '__main__.MyClass2'>, args=('hello',), kwargs={'a': 1, 'b': 2} MyClass2.__new__(cls=<class '__main__.MyClass2'>, s=hello, a=1, b=2) MyClass2.__init__(self=<__main__.MyClass2 object at 0x00000169EED7D860>, s=hello, a=1, b=2) MyMeta.__call__ return: <__main__.MyClass2 object at 0x00000169EED7D860> MyClass instance created: <__main__.MyClass2 object at 0x00000169EED7D860>
class CamelToSnake(type):
def __new__(mcs, name, bases, attrs):
# pick up any attribute that doesn't start with '__' and snakecase it
snake_attrs = {}
for attr_name, attr_val in attrs.items():
if not name.startswith('__'):
snake_attrs[convert(attr_name)] = attr_val
else:
snake_attrs[attr_name] = attr_val
return super().__new__(mcs, name, bases, snake_attrs)
class MyVector(metaclass=CamelToSnake):
def addToVector(self, other): pass
def subtractFromVector(self, other): pass
def calculateDotProduct(self, other): pass
def calculateCrossProduct(self, other): pass
def calculateTripleProduct(self, other): pass
print([a for a in dir(MyVector) if not a.startswith('__')])
['add_to_vector', 'calculate_cross_product', 'calculate_dot_product', 'calculate_triple_product', 'subtract_from_vector']
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class SnakeSingleton(CamelToSnake, Singleton):
pass
class MyVector(metaclass=SnakeSingleton):
def addToVector(self, other): pass
def subtractFromVector(self, other): pass
def calculateDotProduct(self, other): pass
def calculateCrossProduct(self, other): pass
def calculateTripleProduct(self, other): pass
print([a for a in dir(MyVector) if not a.startswith('__')])
v1 = MyVector(); v2 = MyVector()
print(v1 is v2)
['add_to_vector', 'calculate_cross_product', 'calculate_dot_product', 'calculate_triple_product', 'subtract_from_vector'] True