文章目录

  • Python元类详解
  • Python谜团
  • 元类的本质
  • 调用一个类时发生了什么
  • 再探元类
  • 自定义元类
  • 彩蛋:跳过python解释器
  • Python元类详解

    元类比99%的用户所担心的魔法要更深,如果你在犹豫是否需要它们,那你就还不需要它们(真正需要元类的人,能够确定的知道需要它们,并且不必做任何解释)。——Tim Peters

    Python谜团

    相信你一定听说过“Python中一切皆是对象”这句话,只要是一个对象,那么它就有自己的类型(type)。可以使用type查看一个对象的类型:

    $ python3
    >>> a = 2
    >>> type(a)
    <class 'int'>
    >>> b = 'Hello world!'
    >>> type(b)
    <class 'str'>
    

    运行时出现了这样的结果,我们就说:

    2 的类型是 int
    ‘Hello world!’ 的类型是 str

    我们都知道,intstr也都是类,那么,一个类的类型是什么呢?

    $ python3
    >>> type(int)
    <class 'type'>
    >>> type(str)
    <class 'type'>
    >>> type(type)
    <class 'type'>
    

    我们又得到了什么呢?

    int 的类型是 type
    str 的类型是 type
    type 的类型是 type

    这下我们都知道了:type其实也是一个类,并且type是所有类的类型。

    如果一个类是另一个类的类型,我们就称这个类是一个元类(metaclass)

    到这里是不是有点绕?别急,我们接下来就好好讲讲这个神秘的type

    元类的本质

    先来看一段代码:

    class A:
        def __init__(self):
            self.a = 2
    
    a = A()
    print(a)  # <__main__.A object at 0xXXXXXXXX>
    print(a.a)  # 2
    print(type(a))  # <class '__main__.A'>
    print(a.__class__)  # <class '__main__.A'>
    print(A)  # <class '__main__.A'>
    print(type(type(a)))  # <class 'type'>
    print(a.__class__.__class__)  # <class 'type'>
    print(A.__class__)  # <class 'type'>
    

    从以上输出我们可以了解到:A是一个类,我们通过A()得到了一个叫做a的实例(instance)。那如果我们调用一下type类,是不是会得到另一个类呢?我们应该怎样调用呢?我们可以先查看help函数:

    $ python3
    >>> help(type)
    Help on class type in module builtins:
    
    class type(object)
     |  type(object_or_name, bases, dict)
     |  type(object) -> the object's type
     |  type(name, bases, dict) -> a new type
    ...
    

    从以上结果我们了解了,可以给type传入三个参数,分别是类名父类属性字典,就能构造出一个新的类!所谓类的方法只不过是在填充属性字典时把函数名和函数作为健值对填充了而已。让我们来试一下:

    $ python3
    >>> type('A', (), {})  # bases 为空默认继承 object
    <class '__main__.A'>
    >>> A = type('A', (), {})
    >>> a = A()
    >>> a
    <__main__.A object at 0xXXXXXXXX>
    

    上述代码示例中的A是一个类,同时也是一个type类的实例,是通过调用type类时创建出来的。

    调用一个类时发生了什么

    调用一个类指的是在一个类的后面使用一对圆括号使其构造出一个实例。大家应该都知道调用一个类时会自动调用该类的__init__方法,但是仅仅只是调用一下__init__吗?肯定不是!__init__方法只不过是做了一个类调用时的很少部分工作而已。你看,我们即使定义了__init__方法,那也可以直接写一个pass什么都不干。让我们从源代码的角度审视这个问题:

    static PyObject *
    type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
    {  
        // 如果我们调用的是 float
        // 那么显然这里的 type 就是 &PyFloat_Type
        
        // 这里是声明一个PyObject *
        // 显然它是要返回的实例对象的指针
        PyObject *obj;
      
        // 这里会检测 tp_new是否为空,tp_new是什么估计有人已经猜到了
        // 我们说__call__对应底层的tp_call
        // 显然__new__对应底层的tp_new,这里是为实例对象分配空间
        if (type->tp_new == NULL) {
            // tp_new 是一个函数指针,指向具体的构造函数
            // 如果 tp_new 为空,说明它没有构造函数
            // 因此会报错,表示无法创建其实例
            PyErr_Format(PyExc_TypeError,
                         "cannot create '%.100s' instances",
                         type->tp_name);
            return NULL;
        }
      
        // 通过tp_new分配空间
        // 此时实例对象就已经创建完毕了,这里会返回其指针
        obj = type->tp_new(type, args, kwds);
        // 类型检测
        obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);
        if (obj == NULL)
            return NULL;
    
        // 我们说这里的参数type是类型对象,但也可以是元类
        // 元类也是由PyTypeObject结构体实例化得到的
        // 元类在调用的时候执行的依旧是type_call
        // 所以这里是检测type指向的是不是PyType_Type
        // 如果是的话,那么实例化得到的obj就不是实例对象了,而是类型对象
        // 要单独检测一下
        if (type == &PyType_Type &&
            PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
            (kwds == NULL ||
             (PyDict_Check(kwds) && PyDict_GET_SIZE(kwds) == 0)))
            return obj;
    
        // tp_new应该返回相应类型对象的实例对象(的指针)
        // 但如果不是,就直接将这里的obj返回
        // 此处这么做可能有点难理解,我们一会细说
        if (!PyType_IsSubtype(Py_TYPE(obj), type))
            return obj;
      
        // 拿到obj的类型
        type = Py_TYPE(obj);
        // 执行 tp_init
        // 显然这个tp_init就是__init__函数
        // 这与Python中类的实例化过程是一致的。
        if (type->tp_init != NULL) {
            // 将tp_new返回的对象作为self,执行 tp_init
            int res = type->tp_init(obj, args, kwds);
            if (res < 0) {
                // 执行失败,将引入计数减1,然后将obj设置为NULL
                assert(PyErr_Occurred());
                Py_DECREF(obj);
                obj = NULL;
            }
            else {
                assert(!PyErr_Occurred());
            }
        }
        // 返回obj
        return obj;
    }
    

    以上这段代码就是我们调用一个类的时候底层执行的C代码。我们可以看到,再调用__init__方法之前,会先调用__new__方法为对象分配空间,我们也可以重载这个__new__方法:

    class A:
        # __new__ 方法是一个 classmethod
        # 因为调用时实例的空间还没有分配
        # 所以只能传入这个类
        def __new__(cls):
            print('我是__new__')
            # 这里的写法比较固定
            # 因为总是要调用 object 类的 __new__ 方法来分配空间
            return object.__new__(cls)
        def __init__(self):
            print('我是__init__')
    
    a = A()
    '''
    我是__new__
    我是__init__
    '''
    

    你看,在调用__init__方法之前还要调用一遍__new__方法,这与我们在CPython源代码中所看到的是一致的。现在我们来重新审视一下这几行代码:

    static PyObject *
    type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
    {
        ...
        //tp_new应该返回相应类型对象的实例对象(的指针)
        //但如果不是,就直接将这里的obj返回
        if (!PyType_IsSubtype(Py_TYPE(obj), type))
            return obj;
        ...
    }
    

    我们在一般情况下是不会重写__new__方法的,如果重写了,就一定要返回object.__new__(cls)来为对象分配空间,这时上面的这一行if语句就是不成立的。可是如果我们返回了一个其他的值,使上述if语句不成立,那么type_call函数就会提前返回了。我们来写一个例子感受一下:

    class A:
        def __new__(cls):
            return 'a'
    
    a = A()
    print(a)  # a
    print(type(a))  # <class 'str'>
    

    这就是说,我们创建的对象是什么取决于类的__new__方法返回了什么。而类就是元类创建出的对象。我们之前说一个元类调用后返回的应该是一个类,但我们现在知道了,我们创建出来的类是什么取决于这个类的元类的__new__方法返回了什么。

    再探元类

    我们说,调用一个对象的方法的方法有两种,分别是通过对象本身进行调用以及通过这个对象的类调用这个方法:

    class A:
        def hello(self):
            print('Hello!')
    
    a = A()
    # 第1种方法
    a.hello()  # Hello!
    # 第2种方法
    A,hello(a)  # Hello!
    

    那么我们要调用一个类中的某个方法,同样也可以通过元类调用。比如动态增加属性:

    class A: pass
    
    a = A()
    A.x = 1
    print(a.x)  # 1
    type.__setattr__(A, 'y', 2)
    print(a.y)  # 2
    

    自定义元类

    type就是一个元类,那么我们只要让一个类继承自type,那么不就拥有了type类的能力了吗?让我们来试一下:

    class Meta(type): pass
    
    # 元类是这样使用的哦
    class A(metaclass=Meta): pass
    
    print(A)  # <class '__main__.A'>
    print(type(A))  # <class '__main__.Meta'>
    

    因为A的元类变成了Meta,所以A就变成了Meta的实例,那么A的类型就是Meta了。现在我们来把这个元类扩充一下。

    class Meta(type):
        def __new__(cls, name, bases, attrs):
            print('我是__new__')
            return type.__new__(cls, name, bases, attrs)
    
    class A(metaclass=Meta): pass  # 我是__new__
    

    这里我们创建A本质上是使用了和type一样的Meta(name, bases, dict)调用方法,即调用了Meta类。按照上面讲过的类的调用方法,是先调用了Meta.__new__,再调用了Meta.__init__,从而创建出了A。同样,我们也可以使用一个拥有同样调用方法的函数来欺骗解释器。别眨眼,元类也可以不是一个类哦,只要是一个和type有同样调用方法的函数也都是可以的!那么我们现在就来……

    def Meta(name, bases, attrs):
        print('我是一个函数!')
        return type(name, bases, attrs)
    
    class A(metaclass=Meta): pass  # 我是一个函数!
    

    看到了吧,python是一门极其开放的动态型编程语言。还有呢!如果我们让元类的__new__不返回一个类,而是返回一个别的东西,那么……这个类也会变成这个东西:

    class Meta(type):
        def __new__(cls, name, bases, attrs):
            return 123
    
    class A(metaclass=Meta): pass
    
    print(A)  # 123
    print(type(A))  # <class 'int'>
    

    怎么回事?我类呢?!哈哈,这就是python的精妙之处。那么我们这篇文章就接近尾声了,看我写的这么努力 ,各位就点一个赞再走吧。最后附上一个彩蛋哦~

    彩蛋:跳过python解释器

    strtuplelist这样的内置类,都是在底层的C代码中静态写死的,我们是不能对它们随便设置属性的。但是真的是这样吗,python可是一门极其自由的动态性语言哦,话不多说,我们立刻开始尝试:

    $ python3
    >>> str.a = 2
    TypeError: can't set attributes of built-in/extension type 'str'
    >>> a = 'a'
    >>> a.a = 2
    AttributeError: 'str' object has no attribute 'a'
    >>> str.__dict__['a'] = 2
    TypeError: 'mappingproxy' object does not support item assignment
    >>> type.__setattr__(str, 'a', 2)
    TypeError: can't set attributes of built-in/extension type 'str'
    

    啊?连元类都失败了。不行不行,我还有别的办法……

    import gc
    
    gc.get_referents(str.__dict__)[0]['a'] = 2
    # 居然没有报错
    
    print(str.a)  # 2
    a = 'a'
    print(a.a)  # 2
    

    哈!居然成功了!大家知道是什么原理吗?欢迎评论区讨论哦~

    来源:Cosmicland

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python元类详解

    发表评论