No, metaclass. How dare you say you can Python?

python is a magical language. The magical place is that everything is an object, so if you don't have an object, you should be able to find true love here.

What is all objects? What is an object? What does it have to do with class?

In python, when we define classes, we often

class A(object):
    pass

We all know that this object is a parent class. If any class goes back several generations, its ancestors are objects.

What are the types of these classes we define?

print(type(A))
<class 'type'>

The type of class is type, hahaha.

The end of type in python is type. If you don't believe it, look

a = 1
print(type(a))  # int
print(type(type(a)))  # type

Therefore, there are the following statements:

  • Type is the vertex of the object, and all objects are created from type.

  • Object is the vertex inherited by the class, and all classes inherit from object.

We know that we need to define when defining classes__ init__ Method is used to initialize parameters during instantiation, but the real object creation is not in this method, but in__ new__ So we can do it__ new__ Method to intervene in the process of object creation.

What the hell is metaclass for? Since a class is an object, let's think about whether we can interfere when defining a class (object creation)? This is the role of metaclass. Look at an example

class A(type):
    def __new__(cls, *args, **kwargs):
        print('metaclass new')
        return type.__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        print('metaclass init')

class B(metaclass=A):
    def __init__(self):
        print('B init')

print('==start==')
b = B()

output

metaclass new
metaclass init
==start==
B init

Please pay attention to the printing order. Before instantiating b, metaclass has been executed. That is to say, when class B is generated (not instantiated), it is affected by class A.

What's the use of this? What scenes will be used?

ORM (Object Relational Mapping) Object Relational Mapping

When we use Django framework, it is inevitable to involve data management operation (CRUD), and we need to connect with the database. The relational database needs to write native SQL statements, so if our code contains a large number of SQL statements, it will seriously affect the development efficiency and become difficult to maintain.

  • Application developers need to spend a lot of energy to optimize SQL statements
  • SQL differences between databases make it difficult to adapt during database migration

Therefore, Django put forward the concept of ORM and encapsulated the SQL statement object-oriented. Now let's implement it:

# 1, First, define the Field class, which is responsible for saving the Field name and Field type of the database table
class Field:
    def __init__(self, name, column_type):
        self.name = name
        self.colmun_type = column_type

    def __str__(self):
        return f'<{self.__class__.__name__}:{self.name}>'


class StringField(Field):
    def __init__(self, name):
        super().__init__(name, 'varchar(100)')


class IntegerField(Field):
    def __init__(self, name):
        super().__init__(name, 'bigint')


# 2, Define metaclasses to control the creation of Model objects
class ModelMetaClass(type):
    def __new__(cls, name, bases, attrs):
        if name == 'Model':
            return super().__new__(cls, name, bases, attrs)
        mappings = dict()
        for k, v in attrs.items():
            # Keep the mapping relationship between class attributes and columns to the mappings dictionary
            if isinstance(v, Field):
                print(f'Found mapping:{k}==>{v}')
                mappings[k] = v
        for k in mappings.keys():  # Removing the class attribute means that the defined class field does not pollute the User class attribute, and these key s can be accessed only in the instance
            attrs.pop(k)
        attrs['__table__'] = name.lower()  # Assuming that the table name is lowercase of the class name, add one when creating the class__ table__ attribute
        attrs['__mappings__'] = mappings  # Maintain the relationship mapping between attributes and columns, and add one when creating a class__ mappings__ attribute
        return super().__new__(cls, name, bases, attrs)


# 3, Model base class
class Model(dict, metaclass=ModelMetaClass):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(f"'Model' object has no attribute '{key}'")

    def __setattr__(self, key, value):
        self[key] = value

    def save(self):
        fields = []
        params = []
        args = []
        for k, v in self.__mappings__.items():
            fields.append(v.name)
            params.append('?')
            args.append(getattr(self, k, None))
        sql = f"insert into {self.__table__} {','.join(fields)} values ({','.join(params)})"
        print(f'SQL:{sql}')
        print(f'ARGS:{str(args)}')


# We want to create an ORM similar to Django. As long as we define fields, we can operate on database tables and fields
# Finally, we use the defined ORM interface, which is very simple to use
class User(Model):
    id = IntegerField('id')
    name = StringField('username')
    email = StringField('email')
    password = StringField('password')


user = User(id=1, name='Job', email='job@test.com', password='pw')
user.save()

output

Found mapping:id==><IntegerField:id>
Found mapping:name==><StringField:username>
Found mapping:email==><StringField:email>
Found mapping:password==><StringField:password>
SQL:insert into user id,username,email,password values (?,?,?,?)
ARGS:[1, 'Job', 'job@test.com', 'pw']

The Model base class controls the creation of objects through the meta class ModelMetaClass, and then defines a unified save interface, which is very easy and humanized when users define databases and operations. Then we can implement the update, delete and search interfaces.

Metaclass simultaneous implementation__ getattr__ And__ setattr__ Method, you can directly reference ordinary fields

user.id = 1
print(user.id)

Divergent thinking

Can I refactor directly in the Model without metaclass__ new__, Content and ModelMetaClass__ new__ Consistent, can we achieve the same effect?

No, only metaclass__ new__ You can only modify the parameters of subclasses. Other classes cannot, so you can only use metaclass

Dynamic loading

Configuration files are combined with classes to instantiate classes through configuration files. Different configuration files can be instantiated differently. Here is yaml The implementation of yamlobject also uses metaclass.

import time
import yaml


class Monster(yaml.YAMLObject):
    yaml_tag = '!yaml'

    def __init__(self, name):
        self.name = name


while 1:
    with open('dynamicLoad_demo.yaml', 'r') as f:
        cls = yaml.load(f, Loader=yaml.Loader)
    print(cls.name)
    time.sleep(2)

Build the configuration file dynamicload at the same time_ demo. yaml

!yaml
name:
  zzr

The code runs. After a while, modify the name field in the configuration file, and you can see that the output synchronization changes.

reference resources

metaclass in python

Tags: Python Dev

Posted by Stickdragon on Fri, 20 May 2022 02:57:07 +0300