• Python中的元类(metaclass)


    活动地址:CSDN21天学习挑战赛

    metaclass在Python中是个“逆天”的存在,有人认为它是“阿拉丁神灯”,无所不能;有人认为它是“潘多拉魔盒”,会蛊惑程序员去滥用,释放“恶魔”,然后悲剧就产生了。就连硅谷一线大厂要想使用metaclass都得需要特批。深入理解它的Python开发人员占比不到0.1%。

    它会带来好处也容易带来灾难,只有深入了解它,才能使用好它。

    一切皆对象

    类也是对象

    在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段,在Python中这一点仍然成立。但是,Python中的类还远不止如此。类同样也是一种对象。只要你使用关键字class,Python解释器在执行的时候就会创建一个对象。下面的代码段:

    class MyClass(object):
        pass
    
    • 1
    • 2

    将在内存中创建一个对象,名字就是MyClass。这个对象(类)自身拥有创建对象(类实例)的能力,而这就是为什么它是一个类的原因。但是,它的本质仍然是一个对象,于是你可以对它做如下的操作: 你可以将它赋值给一个变量, 你可以拷贝它, 你可以为它增加属性, 你可以将它作为函数参数进行传递。

    在Python中有两种对象:

    • 类型(类)对象:可以被实例化和继承;
    • 非类型(实例)对象:不可以被实例和继承。

    耳熟能详的一句话,Python中一切皆为对象:

    • 在Python里,int整形是对象,整数2也是对象,定义的函数、类都是对象,定义的变量也是对象。总之,在Python里能用到的都可以称之为对象。

    type和object

    明白了Python中一切皆对象之后,再来了解一下Python中对象之间的两种关系。面向对象体系中有两种关系:

    • 父子关系:通常描述为“继承关系的两个类,被继承的那个类是父类,继承的那个类是子类”。
    • 类型实例关系:这种关系存在于两个对象中,其中一个对象(实例)是另一个对象(类型)的具体实现。

    Python中万物皆对象,一个Python对象可能拥有两个属性,__class____bases____class__表示这个对象是谁创建的,__bases__表示这个类的父类__class__type()函数效果一样。

    代码示例:

    class MyClass:
        pass
     
    >>> MyClass.__class__
    type
    >>> MyClass.__bases__
    (object,)
    >>> int.__class__
    type
    >>> int.__bases__
    (object,)
    >>> object.__class__  # object是type的实例,object创建自type
    type
    >>> object.__bases__  # object没有超类,它本身是所以对象的超类
    ()
    >>> type.__class__    # type创建自本身
    type
    >>> type.__bases__    # type继承自object,即object是type的超类
    (object,)  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    从上面的代码可以知道:

    • type对象的顶点,所有对象都创建自type
    • object类继承的顶点,所有类都继承自object

    这是一个重点!

    元类、类、实例

    • object是所有类的超类,而且type也是继承自object;所有对象创建自type,而且object也是type的实例。
    • typeobject的类型,同时,object又是type的超类”,那到底是先有object还是先有type呢?这就像“先有鸡还是先有蛋问题”。
    • objecttype是Python中的两个源对象,事实上,它们是互相依赖对方来定义,所以它们不能分开而论
    • 通过这两个源对象可以繁育出一堆对象:listtupledict等。元类就是这些类的类,这些类是元类的实例。

    metaclass

    type(“造物的上帝”)

    就像str是用来创建字符串对象的类,int是用来创建整数对象的类,而type就是创建类对象的类

    类本身不过是一个名为**type**** 类的实例**。在 Python的类型世界里,type这个类就是造物的上帝。用户自定义类,只不过是type类的__call__运算符的重载。当我们定义一个类的语句结束时,真正发生的情况,是 Python 调用 type__call__运算符。简单来说,当你定义一个类时,写成下面时:

    class MyClass:
      data = 1
    
    • 1
    • 2

    Python 真正执行的是下面这段代码:

    class = type(classname, superclasses, attributedict)
    
    • 1

    这里等号右边的type(classname, superclasses, attributedict),就是type__call__运算符重载,它会进一步调用:

    type.__new__(typeclass, classname, superclasses, attributedict)
    type.__init__(class, classname, superclasses, attributedict)
    
    • 1
    • 2

    这一切都可以通过代码验证,比如下面这段代码示例:

    class MyClass:
        data = 1
        
    >>> instance = MyClass()
    >>> MyClass, instance
    (__main__.MyClass, <__main__.MyClass at 0x1b1b238a688>)
     
    >>> MyClass = type('MyClass', (), {'data': 1})
    >>> instance = MyClass()
    >>> MyClass, instance
    (__main__.MyClass, <__main__.MyClass at 0x1b1b2387708>)
     
    >>> instance.data
    1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    由此可见,正常的 MyClass 定义,和手工去调用type运算符的结果是一样的。

    class type(name, bases, dict)参数详解:

    1. 使用1个参数,返回对象的类型。
      • 就像object.__class__
    2. 使用3个参数,返回一个新类型对象。本质上,这是类声明的一种动态形式。
      • 参数name是一个字符串,表示类名称,并记录为__name__属性;
      • 参数bases是一个元组,一个个记下基础类,并记录为__bases__属性;
      • 参数dict是一个字典,包含类本体的命名空间并被赋值到标准字典。并记录为__dict__属性。

    举个例子:下面两个声明创建了相同类型的对象:

    class X:
        a = 1
        
    X = type('X', (object,), dict(a=1))
    
    • 1
    • 2
    • 3
    • 4

    在Python 3.6发生改变,type的子类不能重写type.__new__ ,不久将不再使用单参数的样式获取对象的类型。

    slots(定位,跟踪)

    当在类中定义一个魔术方法的时候,function除了__dict__中的条目之外,在整个类结构中,作为一个描述着这个类的指针一样结束。这个结构对于每一个魔术方法有一个字段。出于一些原因这些字段被称为type slots
    现在,这里有另一个特征,通过__slots__属性执行,一个拥有__slots__class创造的实例不包含__dict__(这将使用更少的内存)。副作用是实例不能出现未在__slots__中指定的字段:如果你尝试设置一个不存在于__slots__中的字段,那么将会获得一个报错。

    这里提及的单独的slots都是type slots不是__slots__。(类里的魔术方法)

    class Foobar:
    	"""
    	 A class that only allows these attributes: "a", "b" or "c"
    	"""
    	__slots__ = "a", "b", "c"
     
    >>> foo = Foobar()
    >>> foo.a = 1
    >>> # foo.x = 2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    样例中去掉最后一行注释,foo.x = 2会报错。

    metaclass属性

    metaclasstype的子类,通过替换type__call__运算符重载机制,“超越变形”正常的类。其实,理解了以上几点,我们就会明白,正是 Python 的类创建机制,给了metaclass大展身手的机会。

    一旦把一个类型MyClassmetaclass 设置成MyMetaMyClass就不再由原生的type创建,而是会调用MyMeta__call__运算符重载。

    class = type(classname, superclasses, attributedict) 
    # 变为了
    class = MyMeta(classname, superclasses, attributedict)
    
    • 1
    • 2
    • 3

    来看个例子:

    class MyMeta(type):
        def __new__(cls, *args, **kwargs):
            print('===>MyMeta.__new__')
            print(cls.__name__)
            return super().__new__(cls, *args, **kwargs)
     
        def __init__(self, classname, superclasses, attributedict):
            super().__init__(classname, superclasses, attributedict)
            print('===>MyMeta.__init__')
            print(self.__name__)
            print(attributedict)
            print(self.tag)
     
        def __call__(self, *args, **kwargs):
            print('===>MyMeta.__call__')
            obj = self.__new__(self, *args, **kwargs)
            self.__init__(self, *args, **kwargs)
            return obj
     
        
    class Foo(object, metaclass=MyMeta):
        tag = '!Foo'
     
        def __new__(cls, *args, **kwargs):
            print('===>Foo.__new__')
            return super().__new__(cls)
     
        def __init__(self, name):
            print('===>Foo.__init__')
            self.name = name
    # ----------------------输出----------------------
    # ===>MyMeta.__new__
    # MyMeta
    # ===>MyMeta.__init__
    # Foo
    # {'__module__': '__main__', '__qualname__': 'Foo', 'tag': '!Foo', '__new__': , '__init__': , '__classcell__': }
    # !Foo
     
    >>> print('test start')
    >>> foo = Foo('test')
    >>> print('test end')
    # test start
    # ===>MyMeta.__call__
    # ===>Foo.__new__
    # ===>Foo.__init__
    # test end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    在创建Foo类的时候,python做了如下操作:

    1. Foo中有metaclass这个属性吗?如果是,Python会在内存中通过metaclass创建一个名字为Foo类对象
    2. 如果Python没有找到metaclass,它会继续在父类中寻找metaclass属性,并尝试做和前面同样的操作。
    3. 如果Python在任何父类中都找不到metaclass,它就会在模块层次中去寻找metaclass,并尝试做同样的操作。
    4. 如果还是找不到metaclass,Python就会用内置的type来创建这个类对象。

    现在的问题就是,你可以在metaclass中放置些什么代码呢?答案就是:可以创建一个类的东西。那么什么可以用来创建一个类呢?type,或者任何使用到type或者子类化type的东西都可以。用类实现可以(比如上面这个例子),用函数实现也可以。但是metaclass必须返回一个类

    def MyMetaFunction(classname, superclasses, attributedict):
        attributedict['year'] = 2019
        return type(classname, superclasses, attributedict)
     
     
    class Foo(object, metaclass=MyMetaFunction):
        tag = '!Foo'
     
        def __new__(cls, *args, **kwargs):
            print('===>Foo.__new__')
            return super().__new__(cls)
     
        def __init__(self, name):
            print('===>Foo.__init__')
            self.name = name
     
    >>> foo = Foo('test')
    ===>Foo.__new__
    ===>Foo.__init__
    >>> print('name:%s,tag:%s,year:%s' % (foo.name, foo.tag, foo.year))
    name:test,tag:!Foo,year:2019
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    把上面的例子运行完之后就会明白很多了,正常情况下我们在父类中是不能对子类的属性进行操作,但是元类可以。换种方式理解:元类、装饰器、类装饰器都可以归为元编程

    总结

    正常情况下我们在父类中是不能对子类的属性进行操作,但是元类可以,这就使得程序代码的维护变得困难。metaclass是 Python 的黑魔法之一,在掌握它之前不要轻易尝试。感觉上Python的规范原则很松,但这也使得Python更灵活,对代码的编写理应由程序员自己负责,自己写的代码还是得自己负责啊。

    元类、装饰器、类装饰器都可以归为元编程,它们之间有一些相似点,还是在实际的应用中选择比较,使用合适的工具进行编程。

  • 相关阅读:
    KMP算法 (自己复习专用)
    Matlab|【防骗贴】【免费】基于主从博弈的主动配电网阻塞管理
    完美解决api-ms-win-crt-runtime-l1-1-0.dll详细步骤
    【图形学】29 更复杂的光照
    uni-app:实现简易自定义下拉列表
    三剑客之 awk
    【花雕动手做】有趣好玩的音乐可视化系列小项目(22)--LED无限魔方
    【SpringMVC】基础、环境搭建、注解搭建、 and so on……
    STM32CubeMX教程31 USB_DEVICE - HID外设_模拟键盘或鼠标
    搜广推火线入门
  • 原文地址:https://blog.csdn.net/qq_37085158/article/details/126346996