Code 22: Python Metaclasses - Customising Class Creation

Being very meta
coding
python
Author

Tony Phung

Published

December 31, 2024

1. bases and dcts arguments are empty

TonyDynCls1Empty = type("TonyDynCls1Empty",(),{})
print(TonyDynCls1Empty.__name__)
print(TonyDynCls1Empty)
print(TonyDynCls1Empty.__class__.__name__)
print(TonyDynCls1Empty.__base__)
print(TonyDynCls1Empty.__bases__)
print(TonyDynCls1Empty.__class__.__base__)
print(TonyDynCls1Empty.__class__.__bases__)
TonyDynCls1Empty
<class '__main__.TonyDynCls1Empty'>
type
<class 'object'>
(<class 'object'>,)
<class 'object'>
(<class 'object'>,)
class Foo:
    pass

2. bases: (cls1,cls2) and dcts: {attr_1: ...}

Two inherited classes and 1 instance attribute

TonyDynCls2 = type("TonyDynCls2",
                         (TonyDynCls1Empty,Foo), # existing cls
                         {'attr_1':'222'}
                         )
print(TonyDynCls2.__name__)
print(TonyDynCls2)
print(TonyDynCls2.__class__.__name__)
print()
print(TonyDynCls2.__class__.__name__)
print(TonyDynCls2.__base__)
print(TonyDynCls2.__bases__)
print()
print(TonyDynCls2.__class__.__base__)
print(TonyDynCls2.__class__.__bases__)
print()
print(TonyDynCls2.attr_1)
TonyDynCls2
<class '__main__.TonyDynCls2'>
type

type
<class '__main__.TonyDynCls1Empty'>
(<class '__main__.TonyDynCls1Empty'>, <class '__main__.Foo'>)

<class 'object'>
(<class 'object'>,)

222

3. Class Attribute and Lambda Instance Method

TonyDynCls3 = type("TonyDynCls3",
                         (), # existing cls
                         {'attr_1':"333", 
                          'get_attr_1': lambda self: self.attr_1}
                         )
print(TonyDynCls3.__name__)
print(TonyDynCls3)
print(TonyDynCls3.__class__.__name__)
print()
print(TonyDynCls3.attr_1) # class attribute

tony_dc3_instance = TonyDynCls3()
print(tony_dc3_instance.get_attr_1()) # class attribute


TonyDynCls3
<class '__main__.TonyDynCls3'>
type

333
333

4. Class Attribute and Custom Instance Method

def some_method(self):
    return self.attr
TonyDynCls4 = type("TonyDynCls4", (), {'attr': 444,
                                       'get_attr': some_method})

tony_dc4_instance = TonyDynCls4()
print(tony_dc4_instance.attr)
print(tony_dc4_instance.get_attr())
444
444

5. Customising Instance Creation

class Foo:
    pass

    def __new__(cls):
        x = object.__new__(cls)
        x._secret_attr = "555"
        return x
    
a_foo = Foo() 
a_foo._secret_attr
'555'

Note-to-self: psuedo-code steps

    1. by calling type() -> python sees (), looks for type.__call__()
    1. note type.__call__() <——-> type()
    1. or type.__call__(*args, **kwds) <——-> type(*args, **kwds)
    1. inside type.__call__(*args, **kwds)
    1. type.__new__(cls, *args, **kwds)
    1. returns x
    1. type.__init__(x, *args, **kwds)
    1. returns x

6. Customising Class Creation

class TonyMetaClass(type):
    pass

    def __new__(cls, name, bases, dcts):
        x = super().__new__(cls, name, bases, dcts)
        
        # calls parents type.__new__()
        # which is usally called when you instantiate any class
        # e.g Foo()
        # 1. () -> python looks for __call__()
        # 2. find parents `type.__call__()`
        # 3. inside has __new__() and __init__()
        # 4. python will look for __new__() in our cls
        # 5. if cant find, it uses type.__new__()
        # 6. by defining __new__(): we can add custom behaviour
        # 7. super().__new__(cls, name,bases, dcts) is the same
        #    as type.__new__(...), or we are doing nothing new here
        # 8. then we add custom beaviour x._secret_attr = ...
        x._secret_attr = "gday mate"
        
        # 9. return object (as would default type.__new__())
        return x
    
class FooFoo(metaclass=TonyMetaClass):
    pass
foofoo = FooFoo()
foofoo._secret_attr
'gday mate'

7. Simple Object Factory

class FooObjectFactory():
    def __init__(self):
        self.attr = 777
a = FooObjectFactory()        
b = FooObjectFactory()        
c = FooObjectFactory()        
print(a.attr, b.attr, c.attr) # each instance has initialised instance attr
777 777 777

8. Simple Class Factory

class MetaFooClsFactory(type):
    def __new__(cls, name, bases, dcts): # <metaclass> type.__new__() creates the class
        x = super().__new__(cls, name, bases, dcts) 
        x._attr = ["888"] # x, the class itself, an instance type, has class._attr
        return x # return the x instance (the class)

class AFooCls(metaclass = MetaFooClsFactory):
    pass
class BFooCls(metaclass = MetaFooClsFactory):
    pass
class CFooCls(metaclass = MetaFooClsFactory):
    pass
print(AFooCls._attr, BFooCls._attr, CFooCls._attr) # each cls has initialised cls attr
['888'] ['888'] ['888']

9. Simple Inheritance

class Baz():
    cls_attr = '999'

class ABaz(Baz):
    pass
class BBaz(Baz):
    pass
class CBaz(Baz):
    pass

print(ABaz.cls_attr, BBaz.cls_attr, CBaz.cls_attr)
999 999 999

10. Simple Decorator

def tony_decorator(cls):
    class DecoratedClass(cls):
        cls_attr = "10"
    return DecoratedClass

@tony_decorator
class QuxA:
    pass
@tony_decorator
class QuxB:
    pass
@tony_decorator
class QuxC:
    pass

print(QuxA.cls_attr, QuxB.cls_attr, QuxC.cls_attr)
10 10 10