7.5. Metaprogramming Metaclass
Object is an instance of a class
Class is an instance of a Metaclass
Class defines how an object behaves
Metaclass defines how a class behaves
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
When a class definition is executed, the following steps occur:
MRO entries are resolved;
the appropriate metaclass is determined;
the class namespace is prepared;
the class body is executed;
the class object is created.
When using the default metaclass type, or any metaclass that ultimately
calls type.__new__
, the following additional customisation steps are
invoked after creating the class object:
type.__new__
collects all of the descriptors in the class namespace that define a__set_name__()
method;all of these
__set_name__
methods are called with the class being defined and the assigned name of that particular descriptor;the
__init_subclass__()
hook is called on the immediate parent of the new class in its method resolution order. [2]
7.5.1. Recap
Functions are instances of a
function
class
Before we go into metaclasses, let's recap a fact about functions. Consider a typical function definition:
>>> def add(a, b):
... return a + b
Functions are instances of a function
class:
>>> type(add)
<class 'function'>
By extent they are effectively an instance of a type
class.
7.5.2. Class Creation
type(str, tuple, dict)
will create a class object
We need to recall how classes are created:
>>> User = type('User', (), {})
7.5.3. Function Based Metaclass
Metaclass is a function which returns a class
>>> def mytype(clsname, bases, attrs):
... return type('User', (), {})
Then we can use it, instead of a regular type()
:
>>> User = mytype('User', (), {})
7.5.4. Class Based Metaclass
Typically Metaclasses are defined as a class
Metaclass is a class which returns a class from it's constructor
>>> class MyType(type):
... def __new__(metacls, clsname, bases, attrs):
... return type(clsname, bases, attrs)
Then we can use it, instead of a regular type()
:
>>> User = MyType('User', (), {})
7.5.5. Using Metaclasses
Metaclasses are used by setting the
metaclass
keyword argumentThis works for both function and class based metaclasses
For future, we will use the class based metaclasses and
metaclass=...
syntaxIt is exactly equivalent to other ones, but it is more common in practice
Class based metaclass:
>>> class MyType(type):
... def __new__(metacls, clsname, bases, attrs):
... return type(clsname, bases, attrs)
>>>
>>> class User(metaclass=MyType):
... pass
Function based metaclass:
>>> def mytype(clsname, bases, attrs):
... return type('User', (), {})
>>>
>>> class User(metaclass=mytype):
... pass
Both are equivalent and will create a class object.
7.5.6. Rationale
Metaclasses allow you to do 'extra things' when creating a class
Allow customization of class instantiation
Most commonly used as a class-factory
Registering the new class with some registry
Replace the class with something else entirely
Inject logger instance
Injecting static fields
Ensure subclass implementation
Metaclasses run when Python defines class (even if no instance is created)
The potential uses for metaclasses are boundless. Some ideas that have been explored include enum, logging, interface checking, automatic delegation, automatic property creation, proxies, frameworks, and automatic resource locking/synchronization. [2]
>>> class MyType(type):
... def __new__(metacls, clsname, bases, attrs):
... print('before')
... cls = super().__new__(metacls, clsname, bases, attrs)
... print('after')
... return cls
>>> class User(metaclass=MyType):
... pass
...
before
after
Metaclasses allow you to do 'extra things' when creating a class. You can do something before, or after class creation. You can modify a class, add new fields or methods, ensure name is correct, ensure it inherits from some other class, register it in some registry such as event listener, inject named logger instance, or even replace it with something else entirely.
7.5.7. Example - 1
Ensure class name starts with uppercase letter
I have chosen
NameError
exception, but you can choose any other
>>> import string
>>>
>>>
>>> class MyType(type):
... def __new__(metacls, clsname, bases, attrs):
... lowercase_letters = tuple(string.ascii_lowercase)
... if clsname.startswith(lowercase_letters):
... raise NameError('Class name must start with uppercase letter')
... return super().__new__(metacls, clsname, bases, attrs)
This code will work without any problems:
>>> class User(metaclass=MyType):
... pass
But if we change a name of a class to starts with lowercase u
,
then we will receive an exception:
>>> class user(metaclass=MyType):
... pass
...
Traceback (most recent call last):
NameError: Class name must start with uppercase letter
7.5.8. Example - 2
Ensure class inherits from some other class
I have chosen
TypeError
exception, but you can choose any other
>>> class Account:
... pass
>>> class MyType(type):
... def __new__(metacls, clsname, bases, attrs):
... if Account not in bases:
... raise TypeError('All classes must inherit from Account')
... return super().__new__(metacls, clsname, bases, attrs)
This code will work without any problems:
>>> class User(Account, metaclass=MyType):
... pass
If we forget to inherit from Account
class, then we will
receive an exception:
>>> class User(metaclass=MyType):
... pass
...
Traceback (most recent call last):
TypeError: All classes must inherit from Account
7.5.9. Example - 3
Ensure
__init__
method is always present in a classI have chosen
TypeError
exception, but you can choose any other
>>> class MyType(type):
... def __new__(metacls, clsname, bases, attrs):
... if '__init__' not in attrs:
... raise TypeError('All classes must have __init__ method')
... return super().__new__(metacls, clsname, bases, attrs)
This code will work without any problems:
>>> class User(metaclass=MyType):
... def __init__(self):
... pass
If we forget to add __init__
method, then we will
receive an exception:
>>> class User(metaclass=MyType):
... pass
...
Traceback (most recent call last):
TypeError: All classes must have __init__ method
7.5.10. Hiding Metaclass
Metaclasses can be hidden by using inheritance
This is a common pattern in Python frameworks
>>> class MyType(type):
... def __new__(metacls, name, bases, attrs):
... uppercase = tuple(string.ascii_uppercase)
... if not name.startswith(uppercase):
... raise ValueError(f'Class name must start with uppercase letter')
... cls = super().__new__(metacls, name, bases, attrs)
... return cls
>>>
>>> class Account(metaclass=MyType):
... pass
Usage:
>>> class User(Account):
... pass
>>> class user(Account):
... pass
...
Traceback (most recent call last):
ValueError: Class name must start with uppercase letter
7.5.11. Metaclass Arguments
Metaclasses can accept additional arguments
Arguments can be passed to metaclass using
metaclass=...
syntaxThis behavior allows for more customization
>>> from pprint import pprint
>>> import logging
>>> from datetime import datetime, timezone
>>> from uuid import uuid4
>>> class MyType(type):
... def __new__(metacls, clsname, bases, attrs, debug=False):
... cls = super().__new__(metacls, clsname, bases, attrs)
... if debug:
... cls._since = datetime.now(timezone.utc)
... cls._uuid = uuid4()
... cls._log = logging.getLogger(cls.__name__)
... return cls
Creating an object without additional arguments, will create a normal class with typical attributes:
>>> class User(metaclass=MyType):
... pass
>>>
>>> pprint(vars(User))
mappingproxy({'__dict__': <attribute '__dict__' of 'User' objects>,
'__doc__': None,
'__firstlineno__': 1,
'__module__': '__main__',
'__static_attributes__': (),
'__weakref__': <attribute '__weakref__' of 'User' objects>})
Setting debug
to True
, will create a class with additional
attributes, that is _since
, _uuid
, and _log
, which can
enable better debugging:
>>> class User(metaclass=MyType, debug=True):
... pass
>>>
>>> pprint(vars(User))
mappingproxy({'__dict__': <attribute '__dict__' of 'User' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'_log': <Logger User (INFO)>,
'_since': datetime.datetime(2024, 8, 21, 10, 11, 16, 244761, tzinfo=datetime.timezone.utc),
'_uuid': UUID('8eb20432-38dc-4a3b-b798-079285fabde0')})
7.5.12. Metaclass Protocol
__prepare__(metacls, name, bases, **kwargs) -> dict
- on class namespace initialization__new__(metacls, classname, bases, attrs) -> cls
- before class creation__init__(self, name, bases, attrs) -> None
- after class creation__call__(self, *args, **kwargs)
- when the class is called (instantiated)
>>> class MyType(type):
... @classmethod
... def __prepare__(metacls, name, bases) -> 'mappingproxy': ...
... def __new__(metacls, classname, bases, attrs, *args, **kwargs) -> type: ...
... def __init__(self, *args, **kwargs) -> None: ...
... def __call__(self, *args, **kwargs) -> object: ...
Once the appropriate metaclass has been identified, then the class
namespace is prepared. If the metaclass has a __prepare__
attribute,
it is called as namespace = metaclass.__prepare__(name, bases, **kwds)
(where the additional keyword arguments, if any, come from the class
definition). The __prepare__
method should be implemented as a
classmethod()
. The namespace returned by __prepare__
is passed in
to __new__
, but when the final class object is created the namespace
is copied into a new dict
. If the metaclass has no __prepare__
attribute, then the class namespace is initialised as an empty ordered
mapping. [1]
>>> class MyType(type):
... @classmethod
... def __prepare__(metacls, name, bases):
... print('prepare: before')
... result = super().__prepare__(metacls, name, bases)
... print('prepare: after')
... return result
...
... def __new__(metacls, clsname, bases, attrs):
... print('new: before')
... result = super().__new__(metacls, clsname, bases, attrs)
... print('new: after')
... return result
...
... def __init__(self, *args, **kwargs):
... print('init: before')
... super().__init__(*args, **kwargs)
... print('init: after')
...
... def __call__(self, *args, **kwargs):
... print('call: before')
... result = super().__call__(*args, **kwargs)
... print('call: after')
... return result
>>> class User(metaclass=MyType):
... pass
prepare: before
prepare: after
new: before
new: after
init: before
init: after
>>> mark = User()
call: before
call: after
7.5.13. Case Study
class MyType(type):
def __new__(metacls, clsname, bases, attrs):
if Account not in bases:
raise TypeError('All classes must inherit from Account')
return super().__new__(metacls, clsname, bases, attrs)
class Account:
pass
class User(Account, metaclass=MyType):
pass
class Admin(metaclass=MyType):
pass
# Traceback (most recent call last):
# TypeError: All classes must inherit from Account
from string import ascii_uppercase
class MyType(type):
def __new__(metacls, clsname, bases, attrs):
uppercase = tuple(ascii_uppercase)
if not clsname.startswith(uppercase):
raise NameError('All class names must starts with uppercase letter')
return super().__new__(metacls, clsname, bases, attrs)
class User(metaclass=MyType):
pass
class admin(metaclass=MyType):
pass
# Traceback (most recent call last):
# NameError: All class names must starts with uppercase letter
class MyType(type):
def __new__(metacls, clsname, bases, attrs):
if ('AGE_MIN' not in attrs) or ('AGE_MAX' not in attrs):
raise TypeError('All classes must have AGE_MIN and AGE_MAX defined')
return super().__new__(metacls, clsname, bases, attrs)
class User(metaclass=MyType):
AGE_MIN = 18
AGE_MAX = 65
class Admin(metaclass=MyType):
pass
# Traceback (most recent call last):
# TypeError: All classes must have AGE_MIN and AGE_MAX defined
import logging
from datetime import timezone, datetime
from uuid import uuid4
class MyType(type):
def __new__(metacls, clsname, bases, attrs, trace=False):
cls = super().__new__(metacls, clsname, bases, attrs)
if trace:
cls._uuid4 = uuid4()
cls._since = datetime.now(timezone.utc)
cls._log = logging.getLogger(clsname)
return cls
class User(metaclass=MyType):
pass
class Admin(metaclass=MyType, trace=True):
pass
pprint(vars(User))
# mappingproxy({'__dict__': <attribute '__dict__' of 'User' objects>,
# '__doc__': None,
# '__module__': '__main__',
# '__weakref__': <attribute '__weakref__' of 'User' objects>})
pprint(vars(Admin))
# mappingproxy({'__dict__': <attribute '__dict__' of 'Admin' objects>,
# '__doc__': None,
# '__module__': '__main__',
# '__weakref__': <attribute '__weakref__' of 'Admin' objects>,
# '_log': <Logger Admin (INFO)>,
# '_since': datetime.datetime(2024, 8, 7, 8, 15, 58, 999871, tzinfo=datetime.timezone.utc),
# '_uuid4': UUID('9b7bedd0-51d8-44bc-93bb-85fc298299d1')})
from datetime import datetime, timezone
from uuid import uuid4
class Register(type):
listeners = {}
@classmethod
def on_create(cls, *names):
def wrapper(func):
for name in names:
if name not in cls.listeners:
cls.listeners[name] = []
cls.listeners[name] += [func]
return wrapper
def __new__(cls, name, bases, attrs):
for listener in cls.listeners.get(name, []):
name, bases, attrs = listener(name, bases, attrs)
return super().__new__(cls, name, bases, attrs)
@Register.on_create('User', 'Admin')
def debug(name, bases, attrs):
print(f'DEBUG: {name=}')
print(f'DEBUG: {bases=}')
print(f'DEBUG: {attrs=}')
return name, bases, attrs
@Register.on_create('User', 'Admin')
def debug(name, bases, attrs):
print(f'INFO: {name=}')
return name, bases, attrs
@Register.on_create('User')
def trace(name, bases, attrs):
attrs['_uuid4'] = uuid4()
attrs['_since'] = datetime.now(timezone.utc)
return name, bases, attrs
class Account(metaclass=Register):
pass
class User(Account):
pass
# DEBUG: name='User'
# DEBUG: bases=(<class '__main__.Account'>,)
# DEBUG: attrs={'__module__': '__main__', '__qualname__': 'User'}
# INFO: name='User'
class Admin(Account):
pass
# DEBUG: name='Admin'
# DEBUG: bases=(<class '__main__.Account'>,)
# DEBUG: attrs={'__module__': '__main__', '__qualname__': 'Admin'}
# INFO: name='Admin'
pprint(vars(User))
# mappingproxy({'__doc__': None,
# '__module__': '__main__',
# '_since': datetime.datetime(2024, 8, 21, 10, 1, 58, 442815, tzinfo=datetime.timezone.utc),
# '_uuid4': UUID('dd17cdb6-6aec-4afb-9434-54f4297715af')})
pprint(vars(Admin))
# mappingproxy({'__module__': '__main__', '__doc__': None})
# listeners = {
# 'User': [debug, trace],
# 'Admin': [debug],
# }
7.5.14. Use Case - 1
Injecting a named logger instance
>>> import logging
>>>
>>>
>>> class Logger(type):
... def __init__(cls, *args, **kwargs):
... cls._logger = logging.getLogger(cls.__name__)
>>> class User(metaclass=Logger):
... pass
>>>
>>> class Admin(metaclass=Logger):
... pass
>>> print(User._logger)
<Logger User (WARNING)>
>>>
>>> print(Admin._logger)
<Logger Admin (WARNING)>
7.5.15. Instance vs. Class vs. Metaclass
MRO - Method Resolution Order
MRO is a list of classes that Python will use to search for methods
MRO is defined by C3 Linearization algorithm
MRO is defined by
__mro__
attribute
Let's consider a normal (without a metaclass) class:
>>> class User:
... pass
Object is an instance of a class, and an object:
>>> mark = User()
>>>
>>> isinstance(mark, User)
True
>>>
>>> isinstance(mark, object)
True
This is because User
class is an instance of a type
class:
>>> User.__mro__
(<class '__main__.User'>, <class 'object'>)
Now, consider a case with using metaclasses:
>>> class MyType(type):
... pass
>>>
>>> class User(metaclass=MyType):
... pass
Instances for User
class are still instances of a User
class:
>>> mark = User()
>>>
>>> isinstance(mark, User)
True
>>>
>>> isinstance(mark, object)
True
But, they are not an instances of a metaclass:
>>> isinstance(mark, MyType)
False
>>>
>>> isinstance(User, MyType)
True
And a class still inherits from object:
>>> User.__mro__
(<class '__main__.User'>, <class 'object'>)
The following diagram displays the relationship between an instance, class and a metaclass:
7.5.16. Class Bases
Bases for an instance stays the same
Bases of a class changes when using a metaclass
>>> class User:
... pass
>>>
>>>
>>> User.__class__.__bases__
(<class 'object'>,)
>>>
>>> mark = User()
>>> mark.__class__.__bases__
(<class 'object'>,)
Bases for an instance stays the same, but note, how bases of a class changes when using a metaclass:
>>> class MyType(type):
... def __new__(metacls, classname, bases, attrs):
... return type(classname, bases, attrs)
>>>
>>> class User(metaclass=MyType):
... pass
>>>
>>>
>>> User.__class__.__bases__
(<class 'object'>,)
>>>
>>> mark = User()
>>> mark.__class__.__bases__
(<class 'object'>,)
7.5.17. Type Metaclass
All types (user-defined and built-in) are instances of a
type
classThis is also true for an
object
typeEven
type
type is an instance of atype
User defined classes are instances of a type
class:
>>> class User:
... pass
>>>
>>> type(User)
<class 'type'>
All built-in types are instances of a type
class:
>>> type(int)
<class 'type'>
>>>
>>> type(float)
<class 'type'>
>>>
>>> type(bool)
<class 'type'>
>>>
>>> type(str)
<class 'type'>
>>>
>>> type(bytes)
<class 'type'>
>>>
>>> type(list)
<class 'type'>
>>>
>>> type(tuple)
<class 'type'>
>>>
>>> type(set)
<class 'type'>
>>>
>>> type(frozenset)
<class 'type'>
>>>
>>> type(dict)
<class 'type'>
Also an object
is an instance of a type
:
>>> type(object)
<class 'type'>
Even type
type is an instance of a type
:
>>> type(type)
<class 'type'>
7.5.18. Metaclass replacements
Metaclasses allows for more customization, but at a cost of complexity
There are several ways, how you can achieve the same effect
SetUp:
>>> import logging
Inheritance and __init__()
method. This requires calling
super()
in a subclass if __init__()
is overridden:
>>> class Logger:
... def __init__(self):
... self._logger = logging.getLogger(self.__class__.__name__)
>>>
>>> class User(Logger):
... pass
>>>
>>>
>>> mark = User()
>>> print(mark._logger)
<Logger User (WARNING)>
Inheritance and __new__()
method. Overriding __new__
method
is less common, but it is possible. In such case, super()
is
needed:
>>> class Logger:
... def __new__(cls, *args, **kwargs):
... obj = super().__new__(cls)
... obj._logger = logging.getLogger(obj.__class__.__name__)
... return obj
>>>
>>> class User(Logger):
... pass
>>>
>>>
>>> mark = User()
>>> print(mark._logger)
<Logger User (WARNING)>
Class Decorator. This is a more common way to achieve the same effect.
The problem with decorators is that they substitute the class with
a new class Wrapper
, which can lead to problems with inheritance:
>>> def logger(cls):
... class Wrapper(cls):
... _logger = logging.getLogger(cls.__name__)
... return Wrapper
>>>
>>>
>>> @logger
... class User:
... pass
>>>
>>>
>>> mark = User()
>>> print(mark._logger)
<Logger User (WARNING)>
7.5.19. Case Study
>>> import logging
>>> from datetime import datetime, timezone
>>> from uuid import uuid4
Let's create a metaclass EventListener
which enables
to register listeners for class creation.
>>> class OnCreate(type):
... listeners = {}
...
... def __new__(metacls, clsname, bases, attrs):
... for listener in metacls.listeners.get(clsname, []):
... clsname, bases, attrs = listener(clsname, bases, attrs)
... return super().__new__(metacls, clsname, bases, attrs)
...
... @classmethod
... def of(metacls, *clsnames):
... def wrapper(func):
... for clsname in clsnames:
... if clsname not in metacls.listeners:
... metacls.listeners[clsname] = []
... metacls.listeners[clsname] += [func]
... return wrapper
>>> @OnCreate.of('User')
... def trace(clsname, bases, attrs):
... attrs['_since'] = datetime.now(timezone.utc)
... attrs['_uuid'] = uuid4()
... attrs['_log'] = logging.getLogger(clsname)
... return clsname, bases, attrs
>>>
>>> @OnCreate.of('User', 'Admin')
... def info(clsname, bases, attrs):
... print(f'Info: {clsname=}')
... return clsname, bases, attrs
>>>
>>> @OnCreate.of('User', 'Admin')
... def debug(clsname, bases, attrs):
... print(f'Debug: {clsname=}')
... print(f'Debug: {bases=}')
... print(f'Debug: {attrs=}')
... return clsname, bases, attrs
We can inspect what listeners are already registered to class creations:
>>> result = OnCreate.listeners
>>> pprint(result, width=30)
{'Admin': [<function info at 0x14c9db740>,
<function debug at 0x14c9db7e0>],
'User': [<function trace at 0x14c9db6a0>,
<function info at 0x14c9db740>,
<function debug at 0x14c9db7e0>]}
Let's create a Base
class to hide the metaclass complexity:
>>> class Base(metaclass=OnCreate):
... pass
Finally, let's create classes:
>>> class User(Base):
... pass
...
Info: clsname='User'
Debug: clsname='User'
Debug: bases=(<class '__main__.Base'>,)
Debug: attrs={'__module__': '__main__', '__qualname__': 'User', '_since': datetime.datetime(2024, 7, 9, 19, 30, 3, 181585, tzinfo=datetime.timezone.utc), '_uuid': UUID('a20292f4-23c4-4a76-934b-1f2c3fd4967c'), '_log': <Logger User (INFO)>}
>>> class Admin(Base):
... pass
...
Info: clsname='Admin'
Debug: clsname='Admin'
Debug: bases=(<class '__main__.Base'>,)
Debug: attrs={'__module__': '__main__', '__qualname__': 'Admin', '__firstlineno__': 1, '__static_attributes__': ()}
Now we can inspect created classes:
>>> pprint(vars(User))
mappingproxy({'__doc__': None,
'__module__': '__main__',
'_log': <Logger User (INFO)>,
'_since': datetime.datetime(2024, 7, 9, 19, 30, 3, 181585, tzinfo=datetime.timezone.utc),
'_uuid': UUID('a20292f4-23c4-4a76-934b-1f2c3fd4967c')})
>>> pprint(vars(Admin))
mappingproxy({'__doc__': None,
'__firstlineno__': 1,
'__module__': '__main__',
'__static_attributes__': ()})
7.5.20. Use Case - 1
Abstract Base Classes are great example of using metaclasses
ABCMeta
will ensure, that you cannot create an instance of an abstract classABCMeta
at creation ensures that the implementing class covers all abstract methods
SetUp:
>>> from abc import ABCMeta, abstractmethod
ABCMeta
will ensure, that you cannot create an instance of an abstract class:
>>> class Account(metaclass=ABCMeta):
... @abstractmethod
... def login(self):
... ...
>>>
>>>
>>> mark = Account()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Account without an implementation for abstract method 'login'
ABCMeta
at creation ensures that the implementing class covers all abstract methods:
If not, then an exception is raised:
>>> class Account(metaclass=ABCMeta):
... @abstractmethod
... def login(self):
... ...
>>>
>>> class User(Account):
... pass
>>>
>>>
>>> mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User without an implementation for abstract method 'login'
If those two conditions are met, the instance will be created:
>>> class Account(metaclass=ABCMeta):
... @abstractmethod
... def login(self):
... ...
>>>
>>> class User(Account):
... def login(self):
... print('User login')
>>>
>>>
>>> mark = User()
>>> mark.login()
User login
7.5.21. Use Case - 2
Metaclasses can force a class to inherit from a base class
They can automatically inject a base class to the bases
>>> class Account:
... pass
>>>
>>> class MyType(type):
... def __new__(metacls, clsname, bases, attrs):
... if Account not in bases:
... bases += (Account,)
... cls = type(clsname, bases, attrs)
... return cls
Define a class:
>>> class User(metaclass=MyType):
... pass
>>>
>>>
>>> User.mro()
[<class '__main__.User'>, <class '__main__.Account'>, <class 'object'>]
7.5.22. Use Case - 3
Event Listener
>>> class EventListener(type):
... listeners = {}
...
... @classmethod
... def register(metacls, *clsnames):
... def wrapper(func):
... for clsname in clsnames:
... if not clsname in metacls.listeners:
... metacls.listeners[clsname] = []
... metacls.listeners[clsname] += [func]
... return func
... return wrapper
...
... def __new__(metacls, clsname, bases, attrs):
... for listener in metacls.listeners.get(clsname, []):
... clsname, bases, attrs = listener(clsname, bases, attrs)
... return super().__new__(metacls, clsname, bases, attrs)
>>> @EventListener.register('User')
... def debug(clsname, bases, attrs):
... print(f'Debug: {clsname=}')
... print(f'Debug: {bases=}')
... print(f'Debug: {attrs=}')
... return clsname, bases, attrs
...
...
>>> @EventListener.register('User', 'Admin')
... def info(clsname, bases, attrs):
... print(f'Info: {clsname=}')
... return clsname, bases, attrs
>>> EventListener.listeners
{'User': [<function debug at 0x1051c0d60>, <function info at 0x1051c0e00>],
'Admin': [<function info at 0x1051c0e00>]}
>>> class User(metaclass=EventListener):
... pass
...
Debug: clsname='User'
Debug: bases=()
Debug: attrs={'__module__': '__main__', '__qualname__': 'User', '__firstlineno__': 1, '__static_attributes__': ()}
Info: clsname='User'
>>> class Admin(metaclass=EventListener):
... pass
...
Info: clsname='Admin'
7.5.23. Use Case - 5
>>> from datetime import datetime, timezone
>>> import logging
>>> from uuid import uuid4
>>>
>>>
>>> class EventListener(type):
... listeners = {}
...
... @classmethod
... def register(metacls, *clsnames):
... def decorator(func):
... for clsname in clsnames:
... if clsname not in metacls.listeners:
... metacls.listeners[clsname] = []
... metacls.listeners[clsname] += [func]
... return decorator
...
... def __new__(metacls, clsname, bases, attrs):
... listeners = metacls.listeners.get(clsname, [])
... cls = type(clsname, bases, attrs)
... for listener in listeners:
... cls = listener.__call__(cls)
... return cls
Create listener functions and register them for class creation:
>>> @EventListener.register('User', 'Admin')
... def add_logger(cls):
... cls._log = logging.getLogger(cls.__name__)
... return cls
>>>
>>>
>>> @EventListener.register('User')
... def add_trace(cls):
... cls._uuid = str(uuid4())
... cls._since = datetime.now(tz=timezone.utc)
... return cls
Now, define classes with EventListener
metaclass.
>>> class User(metaclass=EventListener):
... pass
>>>
>>> class Admin(metaclass=EventListener):
... pass
>>> pprint(vars(User))
mappingproxy({'__dict__': <attribute '__dict__' of 'User' objects>,
'__doc__': None,
'__firstlineno__': 1,
'__module__': '__main__',
'__static_attributes__': (),
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'_log': <Logger User (WARNING)>,
'_since': datetime.datetime(2024, 12, 1, 11, 32, 45, 680739, tzinfo=datetime.timezone.utc),
'_uuid': 'aa237774-d5a7-448f-bb04-034c06419332'})
>>> pprint(vars(Admin))
mappingproxy({'__dict__': <attribute '__dict__' of 'Admin' objects>,
'__doc__': None,
'__firstlineno__': 1,
'__module__': '__main__',
'__static_attributes__': (),
'__weakref__': <attribute '__weakref__' of 'Admin' objects>,
'_log': <Logger Admin (WARNING)>})
7.5.24. Use Case - 6
Singleton
>>> 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 MyClass(metaclass=Singleton):
... pass
>>> a = MyClass()
>>> b = MyClass()
>>>
>>> a is b
True
>>> id(a)
4375248416
>>>
>>> id(b)
4375248416
7.5.25. Use Case - 7
Final
>>> class Final(type):
... def __new__(metacls, classname, base, attrs):
... for cls in base:
... if isinstance(cls, Final):
... raise TypeError(f'{cls.__name__} is final and cannot inherit from it')
... return type.__new__(metacls, classname, base, attrs)
>>>
>>>
>>> class MyClass(metaclass=Final):
... pass
>>>
>>> class SomeOtherClass(MyClass):
... pass
Traceback (most recent call last):
TypeError: MyClass is final and cannot inherit from it
7.5.26. Use Case - 8
Django
Access static fields of a class, before creating instance:
>>>
... from django.db import models
...
... # class Model(metaclass=...)
... # ...
...
...
... class User(models.Model):
... firstname = models.CharField(max_length=255)
... lastname = models.CharField(max_length=255)