python底层元类小探

原创文章,转载请申明
注:本文所有代码都运行在python3.6环境下

我们都知道,面向对象编程里,实例对象由类创建
那么python中的类是从哪来的呢
答曰:元类

metaclass-relation.png

对象是类的实例
类是元类的实例, 简言之,元类是创建类的底层
乍听有点绕,但是,忽略掉对象,把类想象成对象,把元类想像成类,这样就能好理解一些了

为什么要了解元类?
答曰:更好的理解python,理解编程,更好的写代码
python中,万物皆对象,哪怕是最基础的类型,整型、布尔、字符串等等
查看python中对象的类型使用内建函数type()
但是type函数可不仅仅这么简单

a = 1
b = 'hello'
c = True
print(type(a), a.__class__, a.__class__.__class__)
print(type(b), b.__class__, b.__class__.__class__)
print(type(c), c.__class__, c.__class__.__class__)

# 打印结果如下
  
  
  

可以看到type(对象)对象.__class__都可以获取到对象的类型
那么对象类型是什么类型呢
通过对象__class__.__class__就可以看到类型啦
可以看到基础类型对象的类型的类型都是同一个类型:type
此所谓元类,元类产生类型,类型产生对象
道生一,一生二,二生三,三生万物
python世界里,元类即是一,即类型之源

这里我们介绍type的第二个用法
创建元类

class A(object):
    pass

# 等价于

type('A', (object,), {})

# type 的用法
type(name, bases, attrs)

用class创建的类,都可以用type方法来实现
type第一个参数是创建的类名, 第二个是参数是元组,元组中的是要继承的的父类,第三个参数是字典,里面是所有类的属性的键值对集合,键为属性/方法名的字符串格式,值为属性值/方法体

创建一个带有属性、方法的类:

def add(self, a, b):
    return a + b

name = '运算类'

B = type('Compute', (object,), {'name': name, 'add': add})

b = B()
print(b.name)                      # 运算类
print(b.add(1, 2))                # 3

我们可以创建一个自定义的元类,可以用方法创建、也可以用类创建

def create_metaclass(name, bases, attr):
    print('build meta class by function')
    if not attr.get('zidingyi'):
        attr['zidingyi'] = '这是我自定义的一个属性'
    return type(name, bases, attr)


class A(object, metaclass=create_metaclass):    # 指定类的元类
    pass

a = A()
print(a.zidingyi)              #  这是我自定义的一个属性

普通类用metaclass=xxx的方式来指定元类
注意指定type的三个参数(类名,继承的父类, 类的属性),这样可以返回一个新创建的元类
可以在create_metaclass中对参数做一些处理,定制元类

不过更推荐的方法是用类来创建元类, 此时创建的元类必须要指定父类为type或者继承自type的其他元类

创建的元类内部的流程是,先通过__new__方法返回一个元类对象,再通过__init__初始化, new方法的参数要保持和init方法参数一致
不过init方法可以忽略,这样使用(继承的)type的init方法

class B(type):
    def __new__(cls, name, bases, attr):
        print('create class by B ...')
        return type(name, bases, attr)

class A(object,metaclass=B):
    pass

这里返回的是type创建的元类,不推荐这样做:

class B(type):
    def __new__(cls, name, bases, attr):
        # 使用super调用type
        return super(B, cls).__new__(cls, name, bases, attr)

使用父类type的new方法来创建一个元类实例并返回
注意参数,第一个cls代表当前元类自身,另外三个参数就和type的参数一致啦,再重复一遍,依次为:创建的类名、创建的类要继承的父类元组,创建的类的属性字典

梳理一下创建普通类的过程:

  • 检查类中是否有metaclass属性,如果有,调用metaclass指定的方法或类创建
  • 如果类中没有metaclass属性,会继续在父类寻找
  • 任何父类中都没有,那么就用type创建这个类

创建元类的示例:

class B(type):
    def __new__(cls, name, bases, attr):
        # 通过B创建的类属性都变成大写
        for k, v in attr.items():
            print('attr中的键:', k, 'attr中的值:', v)
            if not k.startswith('__') and isinstance(v, str):
                attr[k] = v.upper()
            else:
                attr[k] = v
        return type(name, bases, attr)

class A(object, metaclass=B):
    name = 'zhangsan'

    def func(self):
        pass

class C(object, metaclass=B):
    name = 'lisi'

print(A.name)                # ZHANGSAN
print(C.name)               # LISI

attr中的键: __module__     attr中的值: __main__
attr中的键: __qualname__   attr中的值: A
attr中的键: name           attr中的值: zhangsan
attr中的键: func           attr中的值: 

attr中的键: __module__     attr中的值: __main__
attr中的键: __qualname__   attr中的值: C
attr中的键: name           attr中的值: lisi

ZHANGSAN
LISI

指定这个自定义的元类,会把创建类中字符串属性转化为大写, 并且打印出创建类中的属性(包含普通属性、方法等)
注意k、v是在元类中upper调用之前打印的,所以此时name的打印是小写

最后玩一个移花接木的方法
这个和元类关系不大,具体是为了解类中new方法的用法
new可以创建一个类实例,第一个参数必须是cls
new的参数和type的参数是相同的(除开第一个参数),真正实现new方法时,其实也会调用type方法的,所以下面的例子和元类还是有些关系的

class YiHua(object):
    def __init__(self):
        print('移花接木类的初始化')

    def hello(self):
        print('移花接木的方法')


class B(object):

    def __init__(self):
        pass

    def __new__(cls, *args, **kwargs):
        instance = super(B, cls).__new__(YiHua, *args, **kwargs)
        instance.__init__(*args, **kwargs)
        return instance

    def hello(self):
        print('被继承类的方法')


class A(B):
    pass

a = A()
a.hello()     # 移花接木的方法

# 以下是打印
移花接木的类的初始化

正常的a类继承自b类,如果调用hello方法,应该是打印被继承类的方法
但是这里却打印YiHua类的hello方法
关键在于b类的new方法中调用了父类的new方法,返回了一个YiHua类的实例,并调用了新实例的初始化方法,所以打印了移花接木类的初始化

看起来很奇特的魔法,而Tornado框架正是使用了这样的技术实现了一个叫 Configurable 的工厂类,用于创建不同网络IO下的epoll还是select模型。

继承自obect类的普通类中,初始化方法的init参数是自定义的
但是继承自type类的元类的init方法参数,不能自定义,必须要和new方法的参数相同,一般不用实现元类的init方法

class B(type):

    def __init__(self):
        pass

    def __new__(cls, name, bases, attr):
        return super(B, cls).__new__(cls, name, bases, attr)


class A(type):

    def __init__(self, name, bases, attr):
        pass

    def __new__(cls, name, bases, attr):
        return super(A, cls).__new__(cls, name, bases, attr)


class C(object, metaclass=B): 
    # TypeError: __init__() takes 1 positional argument but 4 were given
    pass           

B元类会报错, A元类的init才是正确写法,一般可以不用重写init方法
即元类、普通类的init方法也是不同的
参考文章:
https://www.jianshu.com/p/b0f4d9c9afbb
http://kaito-kidd.com/2018/04/19/python-advance-metaclass/

你可能感兴趣的:(python底层元类小探)