Python object-oriented metaclass

Posted by Kold on Mon, 03 Jan 2022 20:57:27 +0100

I. Preface

Metaclasses belong to the deep magic of python object-oriented programming. 99% of people don't know the point. In fact, some people who think they understand metaclasses only justify themselves and point to the end. In terms of the control of metaclasses, they are full of flaws and logical confusion. Today I'll take you to deeply understand the context of python metaclasses.
Behind the author's simple understanding is his obsession with technology day after day. I hope you can respect the originality and be happy that you can solve all your doubts about metaclasses because of this article!!!

What is a metaclass

Everything comes from one sentence: everything in python is an object. Let's first define a class and then analyze it step by step

class OldboyTeacher(object):
    school='oldboy'

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

    def say(self):
        print('%s says welcome to the oldboy to learn Python' %self.name)

All objects are obtained by instantiating or calling the class (the process of calling the class is called class instantiation). For example, object t1 is obtained by calling the class OldboyTeacher

t1=OldboyTeacher('egon',18)
print(type(t1)) #The class of the viewing object t1 is < class'__ main__. OldboyTeacher'>

If everything is an object, the class OldboyTeacher is essentially an object. Since all objects are obtained by calling a class, OldboyTeacher must also be obtained by calling a class, which is called a metaclass
So we can deduce that = = = > the process of generating OldboyTeacher must have happened: OldboyTeacher = metaclass (...)

print(type(OldboyTeacher)) # The result is < class' type '> and it is proved that the OldboyTeacher is generated by calling the metaclass type, that is, the default metaclass is type

Process analysis of creating class with three class keywords

Based on the concept that everything is an object in python, we analyzed that the class we defined with the class keyword is also an object. The class responsible for generating the object is called metaclass (metaclass can be referred to as class class for short), and the built-in metaclass is type
When the class keyword helps us create a class, it must help us call the metaclass OldboyTeacher=type(...). What are the parameters passed in when calling type? It must be the key component of a class. A class has three components, namely
1. Class name_ name=‘OldboyTeacher’
2. Base class_bases=(object,)
3. Class namespace_ DIC, the class namespace is obtained by executing the class body code
When calling type, the above three parameters will be passed in sequence
 
To sum up, the class keyword helps us create a class, which should be subdivided into the following four processes

Supplement: usage of exec

#exec: three parameters

#Parameter 1: string containing a series of python code

#Parameter 2: global scope (dictionary form). If it is not specified, it defaults to global ()

#Parameter 3: local scope (dictionary form). If it is not specified, it defaults to locals()

#The execution of the exec command can be regarded as the execution of a function, and the names generated during the execution will be stored in the local namespace
g={
    'x':1,
    'y':2
}
l={}

exec('''
global x,z
x=100
z=200

m=300
''',g,l)

print(g) #{'x': 100, 'y': 2,'z':200,......}
print(l) #{'m': 300}

V. creation of user-defined metaclass control class OldboyTeacher

A class does not declare its own metaclass. By default, its metaclass is type. In addition to using the built-in metaclass type, we can also define the metaclass by inheriting type, and then specify the metaclass for a class using the metaclass keyword parameter

class Mymeta(type): #Only by inheriting the type class can it be called a metaclass, otherwise it is an ordinary user-defined class
    pass

class OldboyTeacher(object,metaclass=Mymeta): # OldboyTeacher=Mymeta('OldboyTeacher',(object),{...})
    school='oldboy'

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

    def say(self):
        print('%s says welcome to the oldboy to learn Python' %self.name)

Custom meta classes can control the generation of classes. The generation of classes is actually the call process of meta classes, that is, OldboyTeacher=Mymeta("OldboyTeacher", (object), {}). Calling Mymeta first produces an empty object OldoyTeacher, and then passes simultaneous interpreting the parameters in Mymeta brackets to Mymeta under Mymeta. init__ Method to complete the initialization, so we can

class Mymeta(type): #Only by inheriting the type class can it be called a metaclass, otherwise it is an ordinary user-defined class
    def __init__(self,class_name,class_bases,class_dic):
        # print(self) #<class '__main__.OldboyTeacher'>
        # print(class_bases) #(<class 'object'>,)
        # print(class_dic) #{'__module__': '__main__', '__qualname__': 'OldboyTeacher', 'school': 'oldboy', '__init__': <function OldboyTeacher.__init__ at 0x102b95ae8>, 'say': <function OldboyTeacher.say at 0x10621c6a8>}
        super(Mymeta, self).__init__(class_name, class_bases, class_dic)  # Reuse the function of the parent class

        if class_name.islower():
            raise TypeError('Class name%s Please change to hump body' %class_name)

        if '__doc__' not in class_dic or len(class_dic['__doc__'].strip(' \n')) == 0:
            raise TypeError('Class must have a document comment, and the document comment cannot be empty')

class OldboyTeacher(object,metaclass=Mymeta): # OldboyTeacher=Mymeta('OldboyTeacher',(object),{...})
    """
    class OldboyTeacher Document comments for
    """
    school='oldboy'

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

    def say(self):
        print('%s says welcome to the oldboy to learn Python' %self.name)

Call of six custom metaclass control class OldboyTeacher

Reserve knowledge: call

class Foo:
    def __call__(self, *args, **kwargs):
        print(self)
        print(args)
        print(kwargs)

obj=Foo()
#1. To make obj an callable object, you need to define a method in the object's class__ call__ Method, which is automatically triggered when the object is called
#2. The return value of calling obj is__ call__ Method
res=obj(1,2,3,x=1,y=2)

From the above example, calling an object is the trigger in the class where the object is located__ call__ Method. If OldboyTeacher is also regarded as an object, there must be one in the class of OldboyTeacher__ call__ method

class Mymeta(type): #Only by inheriting the type class can it be called a metaclass, otherwise it is an ordinary user-defined class
    def __call__(self, *args, **kwargs):
        print(self) #<class '__main__.OldboyTeacher'>
        print(args) #('egon', 18)
        print(kwargs) #{}
        return 123

class OldboyTeacher(object,metaclass=Mymeta):
    school='oldboy'

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

    def say(self):
        print('%s says welcome to the oldboy to learn Python' %self.name)



# Calling OldboyTeacher is in the calling OldboyTeacher class__ call__ method
# Then pass OldboyTeacher to self, overflow location parameter to *, and overflow keyword parameter to**
# The return value of calling OldboyTeacher is called__ call__ Return value of
t1=OldboyTeacher('egon',18)
print(t1) #123

By default, calling t1=OldboyTeacher('egon ', 18) does three things
1. Generate an empty object obj
2. Call__ init__ Method initializes object obj
3. Returns the initialized obj
Corresponding to the__ call__ Methods should also do these three things

class Mymeta(type): #Only by inheriting the type class can it be called a metaclass, otherwise it is an ordinary user-defined class
    def __call__(self, *args, **kwargs): #self=<class '__main__.OldboyTeacher'>
        #1. Call__ new__ Generate an empty object obj
        obj=self.__new__(self) # self here is an OldoyTeacher class, which must pass parameters, representing the obj object that creates an OldboyTeacher

        #2. Call__ init__ Initialize empty object obj
        self.__init__(obj,*args,**kwargs)

        #3. Returns the initialized object obj
        return obj

class OldboyTeacher(object,metaclass=Mymeta):
    school='oldboy'

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

    def say(self):
        print('%s says welcome to the oldboy to learn Python' %self.name)

t1=OldboyTeacher('egon',18)
print(t1.__dict__) #{'name': 'egon', 'age': 18}

Example above__ call__ Equivalent to a template, we can rewrite it on this basis__ call__ To control the process of calling OldboyTeacher, such as making all the properties of the OldboyTeacher object private

class Mymeta(type): #Only by inheriting the type class can it be called a metaclass, otherwise it is an ordinary user-defined class
    def __call__(self, *args, **kwargs): #self=<class '__main__.OldboyTeacher'>
        #1. Call__ new__ Generate an empty object obj
        obj=self.__new__(self) # self here is an OldoyTeacher class, which must pass parameters, representing the obj object that creates an OldboyTeacher

        #2. Call__ init__ Initialize empty object obj
        self.__init__(obj,*args,**kwargs)

        # After initialization, obj__ dict__ It's worth it
        obj.__dict__={'_%s__%s' %(self.__name__,k):v for k,v in obj.__dict__.items()}
        #3. Returns the initialized object obj
        return obj

class OldboyTeacher(object,metaclass=Mymeta):
    school='oldboy'

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

    def say(self):
        print('%s says welcome to the oldboy to learn Python' %self.name)

t1=OldboyTeacher('egon',18)
print(t1.__dict__) #{'_OldboyTeacher__name': 'egon', '_OldboyTeacher__age': 18}

The above example involves finding attributes, such as self New, see the next section

Sixth, look at attribute search

Combined with the implementation principle of python inheritance + metaclass, what should the attribute search look like???
After learning metaclasses, in fact, all the classes we customize with class are objects (including the object class itself is also an instance of metaclass type, which can be viewed with type(object)). We have learned the implementation principle of inheritance. If we look at the class as an object, the following inheritance should be said: the object OldboyTeacher inherits the object Foo, and the object Foo inherits the object Bar, Object Bar inherits object object

class Mymeta(type): #Only by inheriting the type class can it be called a metaclass, otherwise it is an ordinary user-defined class
    n=444

    def __call__(self, *args, **kwargs): #self=<class '__main__.OldboyTeacher'>
        obj=self.__new__(self)
        self.__init__(obj,*args,**kwargs)
        return obj

class Bar(object):
    n=333

class Foo(Bar):
    n=222

class OldboyTeacher(Foo,metaclass=Mymeta):
    n=111

    school='oldboy'

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

    def say(self):
        print('%s says welcome to the oldboy to learn Python' %self.name)


print(OldboyTeacher.n) #Annotate n=xxx in each class from bottom to top, and then re run the program. It is found that the search order of n is oldboyteacher - > foo - > bar - > Object - > mymeta - > type

Therefore, attribute search should be divided into two layers. One layer is the search of object layer (MRO based on c3 algorithm), and the other layer is the search of class layer (i.e. metaclass layer)

#Search order:
#1. First object layer: oldoyteacher - > foo - > bar - > object
#2. Then metaclass layer: mymeta - > type

Based on the above summary, let's analyze the meta class Mymeta__ call__ Self__ new__ Search for

class Mymeta(type): 
    n=444

    def __call__(self, *args, **kwargs): #self=<class '__main__.OldboyTeacher'>
        obj=self.__new__(self)
        print(self.__new__ is object.__new__) #True


class Bar(object):
    n=333

    # def __new__(cls, *args, **kwargs):
    #     print('Bar.__new__')

class Foo(Bar):
    n=222

    # def __new__(cls, *args, **kwargs):
    #     print('Foo.__new__')

class OldboyTeacher(Foo,metaclass=Mymeta):
    n=111

    school='oldboy'

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

    def say(self):
        print('%s says welcome to the oldboy to learn Python' %self.name)


    # def __new__(cls, *args, **kwargs):
    #     print('OldboyTeacher.__new__')


OldboyTeacher('egon',18) #In the class that triggers OldboyTeacher__ call__ Method, and then execute self__ new__ Start finding

Summary, under Mymeta__ call__ Self new__ I didn't find it in Oldboy teacher, Foo or Bar__ new__ In case of, I will go to the object__ New, and there is one under object by default__ new__, So even the previous classes are not implemented__ new__, You will definitely find one in the object. You won't and don't need to find it in the metaclass Mymeta - > type__ new__
 
We're in metaclass__ call__ You can also use object New (self) to create objects

But we still recommend__ call__ Use self in New (self) to create an empty object, because this method will retrieve three classes oldboyteacher - > foo - > bar, and object__ new__ Directly across the three of them
Finally

class Mymeta(type): #Only by inheriting the type class can it be called a metaclass, otherwise it is an ordinary user-defined class
    n=444

    def __new__(cls, *args, **kwargs):
        obj=type.__new__(cls,*args,**kwargs) # This value transfer method must be followed
        print(obj.__dict__)
        # return obj # The following is triggered only when the return value is an object of type__ init__
        return 123

    def __init__(self,class_name,class_bases,class_dic):
        print('run. . . ')


class OldboyTeacher(object,metaclass=Mymeta): #OldboyTeacher=Mymeta('OldboyTeacher',(object),{...})
    n=111

    school='oldboy'

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

    def say(self):
        print('%s says welcome to the oldboy to learn Python' %self.name)


print(type(Mymeta)) #<class 'type'>
# The process of generating the class OldboyTeacher is to call Mymeta, which is also an object of the type class. Therefore, the reason why Mymeta can be called must be that there is an object in the metaclass type__ call__ method
# At least three things also need to be done in this method:
# class type:
#     def __call__(self, *args, **kwargs): #self=<class '__main__.Mymeta'>
#         obj=self.__new__(self,*args,**kwargs) # An object that generates Mymeta
#         self.__init__(obj,*args,**kwargs) 
#         return obj

Seven exercises

Exercise 1: in the metaclass, control to make the data attributes of the user-defined class uppercase

class Mymetaclass(type):
    def __new__(cls,name,bases,attrs):
        update_attrs={}
        for k,v in attrs.items():
            if not callable(v) and not k.startswith('__'):
                update_attrs[k.upper()]=v
            else:
                update_attrs[k]=v
        return type.__new__(cls,name,bases,update_attrs)

class Chinese(metaclass=Mymetaclass):
    country='China'
    tag='Legend of the Dragon' #Legend of the Dragon
    def walk(self):
        print('%s is walking' %self.name)


print(Chinese.__dict__)
'''
{'__module__': '__main__',
 'COUNTRY': 'China', 
 'TAG': 'Legend of the Dragon',
 'walk': <function Chinese.walk at 0x0000000001E7B950>,
 '__dict__': <attribute '__dict__' of 'Chinese' objects>,                                         
 '__weakref__': <attribute '__weakref__' of 'Chinese' objects>,
 '__doc__': None}
'''

Exercise 2: controlling custom classes in metaclasses does not require__ init__ method
  1. Metaclasses help create objects and initialize them;
  2. It is required that the parameters passed during instantiation must be in the form of keywords, otherwise an exception will be thrown TypeError: must use keyword argument
  3.key is used as a user-defined class to generate the attributes of the object, and all attributes become uppercase

class Mymetaclass(type):
    # def __new__(cls,name,bases,attrs):
    #     update_attrs={}
    #     for k,v in attrs.items():
    #         if not callable(v) and not k.startswith('__'):
    #             update_attrs[k.upper()]=v
    #         else:
    #             update_attrs[k]=v
    #     return type.__new__(cls,name,bases,update_attrs)

    def __call__(self, *args, **kwargs):
        if args:
            raise TypeError('must use keyword argument for key function')
        obj = object.__new__(self) #Create an object, and self is the class Foo

        for k,v in kwargs.items():
            obj.__dict__[k.upper()]=v
        return obj

class Chinese(metaclass=Mymetaclass):
    country='China'
    tag='Legend of the Dragon' #Legend of the Dragon
    def walk(self):
        print('%s is walking' %self.name)


p=Chinese(name='egon',age=18,sex='male')
print(p.__dict__)

Exercise 3: in the metaclass, all the attributes related to the object generated by the user-defined class are hidden attributes

class Mymeta(type):
    def __init__(self,class_name,class_bases,class_dic):
        #Create control class Foo
        super(Mymeta,self).__init__(class_name,class_bases,class_dic)

    def __call__(self, *args, **kwargs):
        #Controls the call process of Foo, that is, the generation process of Foo objects
        obj = self.__new__(self)
        self.__init__(obj, *args, **kwargs)
        obj.__dict__={'_%s__%s' %(self.__name__,k):v for k,v in obj.__dict__.items()}

        return obj

class Foo(object,metaclass=Mymeta):  # Foo=Mymeta(...)
    def __init__(self, name, age,sex):
        self.name=name
        self.age=age
        self.sex=sex


obj=Foo('egon',18,'male')
print(obj.__dict__)

Exercise 4: implement singleton pattern based on metaclass

#Step 5: implement singleton pattern based on metaclass
# Single instance: that is, a single instance. It means that the result of multiple instantiations of the same class points to the same object, which is used to save memory space
# If we read the configuration from the configuration file for instantiation, there is no need to generate objects repeatedly and waste memory when the configuration is the same
#settings. The contents of the PY file are as follows
HOST='1.1.1.1'
PORT=3306

#Method 1: define a class method to implement the singleton pattern
import settings

class Mysql:
    __instance=None
    def __init__(self,host,port):
        self.host=host
        self.port=port

    @classmethod
    def singleton(cls):
        if not cls.__instance:
            cls.__instance=cls(settings.HOST,settings.PORT)
        return cls.__instance


obj1=Mysql('1.1.1.2',3306)
obj2=Mysql('1.1.1.3',3307)
print(obj1 is obj2) #False

obj3=Mysql.singleton()
obj4=Mysql.singleton()
print(obj3 is obj4) #True



#Method 2: customize the metaclass to implement the singleton mode
import settings

class Mymeta(type):
    def __init__(self,name,bases,dic): #Triggered when the class Mysql is defined

        # Take the configuration from the configuration file in advance to create an instance of Mysql
        self.__instance = object.__new__(self)  # Generate object
        self.__init__(self.__instance, settings.HOST, settings.PORT)  # Initialize object
        # The above two steps can be combined into the following step
        # self.__instance=super().__call__(*args,**kwargs)


        super().__init__(name,bases,dic)

    def __call__(self, *args, **kwargs): #Mysql(...) Time trigger
        if args or kwargs: # There is a value in args or kwargs
            obj=object.__new__(self)
            self.__init__(obj,*args,**kwargs)
            return obj

        return self.__instance




class Mysql(metaclass=Mymeta):
    def __init__(self,host,port):
        self.host=host
        self.port=port



obj1=Mysql() # If no value is passed, the configuration is read from the configuration file by default to instantiate. All instances should point to a memory address
obj2=Mysql()
obj3=Mysql()

print(obj1 is obj2 is obj3)

obj4=Mysql('1.1.1.4',3307)



#Method 3: define a decorator to implement the singleton mode
import settings

def singleton(cls): #cls=Mysql
    _instance=cls(settings.HOST,settings.PORT)

    def wrapper(*args,**kwargs):
        if args or kwargs:
            obj=cls(*args,**kwargs)
            return obj
        return _instance

    return wrapper


@singleton # Mysql=singleton(Mysql)
class Mysql:
    def __init__(self,host,port):
        self.host=host
        self.port=port

obj1=Mysql()
obj2=Mysql()
obj3=Mysql()
print(obj1 is obj2 is obj3) #True

obj4=Mysql('1.1.1.3',3307)
obj5=Mysql('1.1.1.4',3308)
print(obj3 is obj4) #False

Topics: Python Back-end