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__)

# 打印结果如下
<class 'int'> <class 'int'> <class 'type'>
<class 'str'> <class 'str'> <class 'type'>
<class 'bool'> <class 'bool'> <class 'type'>

可以看到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中的值: <function A.func at 0x0000026CD97E01E0>

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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352