This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together

preface

Recently, I have been reading Python object-oriented programming, and I got stuck in the knowledge point of metaclass. After all kinds of information query and learning, I have this article, or the same sentence, my ability is limited, if there is any mistake, I hope to criticize and correct, thank you.

The metaclass concept

The concept of metaclasses is very simple.

A class that generates a class is a metaclass.

As we all know, objects are instantiated by classes, as follows.

class Foo:
    pass


foo = Foo()
print(foo)

<__main__.Foo object at 0x7fd9280ef250>
Copy the code

So let’s think about who created the class, and we’ll use the type function to see what it is.

class Foo:
    pass


foo = Foo()
print(type(foo))
print(type(Foo))

<class '__main__.Foo'>
<class 'type'>
Copy the code

We find that class Foo is of type type, so let’s look at python’s native data types.

print(type(int))
print(type(str))

<class 'type'>
<class 'type'>
Copy the code

We can see that type is also type, so type is metaclass. The Type metaclass instantiates classes (class objects), which in turn instantiate objects. So everything in Python is an object, both user-defined classes and Python classes are instantiated with Type.

Type, create the class

We usually define classes using the class keyword.

class Foo:

    i = 1

    def test(self):
        print('test')
Copy the code

The type metaclass should be able to instantiate the class directly, with the syntax:

Type (class name, tuple of the parent class (which can be empty in case of inheritance), dictionary of attributes (name and value)Copy the code
i = 1


def test(self):
    print('test')


type('Foo', (), {'i': i, 'test': test})
Copy the code

Custom metaclasses

We can also define a custom metaclass. All we need to do is inherit the Type class from the custom metaclass, and then specify the custom metaclass in the generated class.

class MyType(type):
    pass

class Foo(metaclass=MyType):
    pass

print(type(Foo))

<class '__main__.MyType'>
Copy the code

The purpose of a custom metaclass is to implement some functionality that can be applied to instantiated classes so that all instantiated classes have the same functionality. Before writing a custom metaclass, it is necessary to explain the __new__, __init__, and __call__ magic methods. Only by understanding and using these methods can we make our custom metaclass do what we want.

__new__ magic method

When I talk about these magic methods, I always talk about them from two different perspectives: ordinary classes and custom metaclasses.

First, in a normal class, the new magic method is a constructor that instantiates an object.

class Foo:

    a = 1

    def __new__(cls, *args, **kwargs):
        print(cls)
        print(args)
        print(kwargs)
        print(object.__new__(cls))
        return object.__new__(cls)

    def test(self):
        print('test')


foo = Foo()

<class '__main__.Foo'>
()
{}
<__main__.Foo object at 0x7f96380db160>
Copy the code
  • Trigger time: when objects are instantiated
  • Action: Instantiates an object
  • Parameters: CLS is the current class, args, and kwargs is the initialization parameter.
  • Return value: instantiated object

In a custom metaclass, the new magic method constructs the class (class object) with the same arguments as the type function constructs the class.

class MyType(type):

    def __new__(mcs, name, bases, dicts):
        print(mcs)
        print(name)
        print(bases)
        print(dicts)
        print(super().__new__(mcs, name, bases, dicts))
        return super().__new__(mcs, name, bases, dicts)


class Foo(metaclass=MyType):
    a = 1

    def __new__(cls, *args, **kwargs):
        return object.__new__(cls)

    def test(self):
        print('test')


foo = Foo()

<class '__main__.MyType'>
Foo
()
{'__module__': '__main__', '__qualname__': 'Foo', 'a': 1, '__new__': <function Foo.__new__ at 0x7fca60176790>, 'test': <function Foo.test at 0x7fca60176820>}
<class '__main__.Foo'>
Copy the code
  • Trigger time: when the class is instantiated
  • Action: Instantiates a class.
  • Parameters: MCS is the current metaclass, name is the name of the instantiated class, bases is the inherited class name of the instantiated class, and dicts is the attributes and methods of the instantiated class.
  • Return value: instantiated class.

I can do a lot of custom things with the new magic method. For example, I want the properties of the instantiated class to be capitalized.

class MyType(type):

    def __new__(mcs, name, bases, dicts):
        attrs = ((name, value) for name, value in dicts.items() if not name.startswith('__'))
        dicts = dict((name.upper(), value) for name, value in attrs)
        return super().__new__(mcs, name, bases, dicts)


class Foo(metaclass=MyType):
    a = 1

    def __new__(cls, *args, **kwargs):
        return object.__new__(cls)

    def test(self):
        print('test')


print(Foo.__dict__)

{'A': 1, 'TEST': <function Foo.test at 0x7fb0580b6820>, '__module__': '__main__', '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None}
Copy the code
__init__ magic method

In a normal class, it’s the initializer method, self, that instantiates the object for the class, and this makes sense, because we’ve been dealing with this a lot.

class Foo:

    a = 1

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

    def test(self):
        print('test')


foo = Foo('li')
print(foo.name)

li
Copy the code

In a custom metaclass, that’s the initialization class object.

class MyType(type):

    def __new__(mcs, name, bases, dicts):
        return super().__new__(mcs, name, bases, dicts)

    def __init__(cls, name, bases, dicts):
        print(cls)
        super().__init__(name, bases, dicts)


class Foo(metaclass=MyType):
    a = 1

    def __new__(cls, *args, **kwargs):
        return object.__new__(cls)

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

    def test(self):
        print('test')

<class '__main__.Foo'>
Copy the code
__call__ magic method

In a normal class, the Call method fires when parentheses are placed on the instantiated object.

class Foo:

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

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


foo = Foo('li')
foo()

<__main__.Foo object at 0x7fbd2806f250>
()
{}
Copy the code

In a custom metaclass, we know that the class is generated by a metaclass, and that class parentheses trigger the Call method.

Foo = MyType() Foo() equals MyType()()Copy the code
class MyType(type):

    def __new__(mcs, name, bases, dicts):
        return super().__new__(mcs, name, bases, dicts)

    def __init__(cls, name, bases, dicts):
        print(cls)
        super().__init__(name, bases, dicts)

    def __call__(cls, *args, **kwargs):
        # obj = cls.__new__(cls)
        # cls.__init__(obj, *args, **kwargs)
        return type.__call__(cls, *args, **kwargs)



class Foo(metaclass=MyType):
    a = 1

    def __new__(cls, *args, **kwargs):
        print('foo new')
        return object.__new__(cls)

    def __init__(self, name):
        print('foo init')
        self.name = name

    def test(self):
        print('test')


foo = Foo('li')
print(foo.__dict__)

<class '__main__.Foo'>
foo new
foo init
{'name': 'li'}
Copy the code

We can see that calling the call function of Type has the same result as manually invoking the new and init methods of the class.

The singleton pattern

Finally, we implement the singleton pattern with metaclasses. The singleton pattern is that only one object is instantiated. In simple terms, if the object is instantiated, the instance is returned, so that there is only one instance.

class MyType(type):

    def __init__(cls, name, bases, dicts):
        print('init')
        cls.__instance = None
        super().__init__(name, bases, dicts)

    def __call__(cls, *args, **kwargs):
        print('call')
        if cls.__instance is None:
            cls.__instance = type.__call__(cls, *args, **kwargs)
        return cls.__instance


class Foo(metaclass=MyType):
    pass


foo1 = Foo()
foo2 = Foo()
print(id(foo1), id(foo2))

init
call
call
140402884588256 140402884588256
Copy the code

That’s it for today. See you next time