<jupyter_start><jupyter_text>It's metaclasses all the way down: Understanding and using Python's metaprogramming facilities  What is metaprogramming?>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- Python has the ability to introspect its basic elements such as functions, classes, or types and to create or modify them on the fly.- Higher order functions allow us to add/modify functionality of existing functions, methods, or classes.- Special methods of classes that allow us to interfere with the creation of class objects. These are called **Metaclasses** and allow us to even completely redesign the Python's object-oriented paradigm. - A selection of different tools that allow programmers to work directly with code either in its raw plain text format or in the more programmatically accessible **Abstract Syntax Tree (AST)** form. This approach is allows for really extraordinary things, such as extending Python's language syntax or even creating your own Domain Specific Language (DSL), see e.g. [Hy](https://github.com/hylang/hy).  Why would you use metaclasses?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). **<NAME>**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**  Quick example 1: Django ORMA typical use for a metaclass is creating an API (e.g. the Django ORM). It allows us to define something like this:```pythonclass Person(models.Model):    name = models.CharField(max_length=30)    age = models.IntegerField()```But if we do this:```pythonguy = Person(name='bob', age=35)print(guy.age)```- It won't return an `IntegerField` object. It will return an `int`, and can even take it directly from the database.- This is possible because `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.- Without specifying an `__init__` method, I am able to instantiate a `Person` object. This is done through the metaclass.- Django makes something complex look simple by exposing a simple API and using metaclasses, recreating code from this API to do the real job behind the scenes.  Quick example 2: Abstract Base Classes- Python does not have special syntax for abstract classes.- We are able to implement this behavior using metaclasses.- Classes that use the meta-class can use `@abstractmethod` and `@abstractproperty` to define abstract methods and properties.- The metaclass will ensure that derived classes override the abstract methods and properties.- Also, classes that implement the ABC without actually inheriting from it can register as implementing the interface, so that `issubclass` and `isinstance` will work.- When we want to enforce contracts between classes in python just as interfaces do in Java, abstract base classes is the way to go.<jupyter_code>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)<jupyter_output>Can't instantiate abstract class Car with abstract methods change_gear, start_engine<jupyter_text>You are using them already- You are using metaclasses even if you are not aware of using them.- They make things possible in Python that wouldn't be otherwise or would require substantial changes to the language.- You are a programmer willing to go out of his comfort zone, learn new things and challenge common practices.but but but... I want to make my own metaclasses!  OK, I got a simple problem for you- You are friends with some guys, they are quite good programmers.- The learned Python but all have a Javascript background (you know, from that **other** meetup).- They wrote a Python library that you really want to use.- They used `camelCase` for all their methods instead of `underscore_method_names`.- You really want to use their library but love `get_first_name` and hate `getFirstName`  Possible solutions1. Override `__getattr__`:    - Renaming must happen everytime you access the method.    - When trying to get attribute/method of an object, the object's superclasses are looked up **before** `__getattr__`. Problems with inherited methods.2. Introspection. Use `inspect.getmembers(MyClass, inspect.isfunction)` and rename functions. This works but:    - You need to run this everytime you load each class.    - Is too specific. What if you want to add extra functionality in how this classes operate (e.g. make a class Singleton or make each class register itself somewhere when it's being used)?3. Class decorator:    - Need to redefine a new nested class for every class you want to alter.    - You will run into problems with inheritance.4. Use metaclasses.  Warning! Metaclasses ahead!  Classes as objects- In most languages, classes are just pieces of code that describe how to produce an object.- But classes are more than that in Python. Classes are objects too.- This object (the class) is itself capable of creating objects (the instances), and this is why it's a class. But still, it's an object, and therefore:    - You can assign it to a variable.    - You can copy it.    - You can add attributes to it.    - You can pass it as a function parameter.<jupyter_code>class ObjectCreator:
    pass
print(ObjectCreator)  # you can pass a class as a parameter because it's an object
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)
ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
print(ObjectCreatorMirror.new_attribute)
print(ObjectCreatorMirror())<jupyter_output>foo
<__main__.ObjectCreator object at 0x00000132FE593780><jupyter_text>Creating classes dynamically1. Remember the function `type`? The good old function that lets you know what type an object is.2. `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:```pythonclass Foo:    bar = True```is equivalent with:```pythonFoo = type('Foo', (), {'bar':True})```  What are metaclasses1. Metaclasses are the "stuff" that creates classes. You define classes in order to create objects, right?2. We learned that Python classes are objects.3. Well, metaclasses are what creates these objects. They are the classes' classes, you can picture them this way:`MyClass = MetaClass()`, `MyObject = MyClass()`  What can act as a metaclass?- Something that can create a class, i.e. anything that subclasses or uses `type`.- Any python **callable**. A callable is an object that can be invoked with the function operator `()`.    - Any Python class. `MyClass()` calls the `__init__()` function of the class    - Any Python function or instance whose class implements `__call__()`. `foo()` is equivalent to `foo.__call__()`. This is true in both cases (i.e. all functions objects implement `__call__`).- i.e. a metaclass is something that we call with some parameters and returns a class object- When you write:```pythonclass A:    ...```you are actually *not creating* the class, you *describe the class* and `type` creates this class object for you.- Each class can have **one** metaclass, however this metaclass can subclass **multiple** metaclasses.  Our example with a function as metaclass<jupyter_code>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 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('__')])<jupyter_output>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']<jupyter_text>Instance creation: `__new__` and `__init__`<jupyter_code>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))
a = MyClass1(1, 2, 3, x='ex', y='why')<jupyter_output>MyClass1.__new__(<class '__main__.MyClass1'>, *(1, 2, 3), **{'y': 'why', 'x': 'ex'})
MyClass1.__init__(<__main__.MyClass1 object at 0x00000132FE5A4E80>, *(1, 2, 3), **{'y': 'why', 'x': 'ex'})<jupyter_text>Class creation: `__prepare__`, `__new__`, `__init__`, `__call__`<jupyter_code>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")
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")
a = MyClass2('hello', a=1, b=2)
print("MyClass instance created: ", a)<jupyter_output>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 0x00000132FE5A9E48>, s=hello, a=1, b=2)
MyMeta.__call__ return:  <__main__.MyClass2 object at 0x00000132FE5A9E48>
MyClass instance created:  <__main__.MyClass2 object at 0x00000132FE5A9E48><jupyter_text>Our example with a class as metaclass<jupyter_code>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('__')])<jupyter_output>['add_to_vector', 'calculate_cross_product', 'calculate_dot_product', 'calculate_triple_product', 'subtract_from_vector']<jupyter_text>Make our class a Singleton<jupyter_code>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)<jupyter_output>['add_to_vector', 'calculate_cross_product', 'calculate_dot_product', 'calculate_triple_product', 'subtract_from_vector']
True
{'fraction_non_alphanumeric': 0.09872830921505983, 'fraction_numerical': 0.008707636409196267, 'mean_word_length': 4.164671627305079, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 34, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '332783', 'license': 'MIT', 'n_tokens_mistral': 4610, 'n_tokens_neox': 4455, 'n_words': 1872, 'path': '015 - Breaking Benjamin/meta_alltheway/meta_alltheway.ipynb', 'repo_name': 'orfeasa/pythess-files', 'size': '15967'}