python学习笔记——元类

写在前面

这两天仔细研究了python中元类的概念,从最开始的一头雾水,到现在的渐渐有一点明白。想借这篇文章来阐述一下我对于python中元类的一些粗浅见解,同时也希望能给其他人一些启发,共同进步。首先,我想说在理解元类时一定要在大脑中时刻按照OOP的编程理念对程序进行分析才能够理解元类中一些比较难以理解的地方,这也是笔者学习元类的一个小经验。

什么是元类

元类在python中最简单的定义就是——type的子类。为了弄清楚这个模糊的定义,首先要弄清楚的是什么是type类,这对于后面的理解是非常重要的。type顾名思义就是“类型”的意思,它和python标准库中的其他类一样也是一个类,但是它也有非常特殊的地方。其他的类的实例就是一个普通的实例,而类型的实例是一个类。这是python3.0中一个更新的点,在2.X版本的python中类型的实例还不完全是一个类,在某些场景下它的实例也是一个普通的实例,不过今天我们要谈的不是2.X版本中的type。让我们用几句简单的代码来验证一下:



在代码中首先构造了一个list实例,然后用type的构造函数以list的实例为参数构造了一个type的实例,打印后我们发现type的实例是一个类,这和list实例的类(l.__class__)是相同的。这就印证了我们的一个论断——类型的实例就是类。

熟悉OOP的人特别是C++的人都应该了解,类只是声明而不是实例,只有在程序中实例化一个类才会分配内存。但是python中类也是一个叫做类对象的对象,它不是凭空出现在程序中的,它和实例一样也需要有代码对类对象进行实例化,说穿了类对象应该也是一种特殊的实例,为什么这么说呢?图一中的代码应该能给我们些许暗示:通过type的构造函数构造出类型的实例是一个类,或者说类对象(不能再细说了,再说就绕进去了,读者自己意会吧)

那么,写到这里我们就可以说type类负责类对象的创建,一般情况下这种创建是隐式的不被我们发觉的。而如果我们要显式地观察这个过程就可以通过创建元类来实现。在OOP中拦截一个类方法的方式之一就是继承这个类并且重载需要拦截的方法,那么这里就可以引出最开始给出的元类定义,元类就是type的子类。元类通过继承type类,进而重载type类中的一些方法来达到控制类对象生成的目的,这是元类编程中一个大体的思想。

类对象的创建过程

类对象的创建过程和实例的创建过程相似或者说大体上是一致的,我们通过一个例子来了解吧。



打印效果如下:



可见在类对象创建时首先调用元类中的__new__方法,然后调用__init__方法。其中__new__方法返回类对象的实例,__init__方法对类对象进行一些初始化。这两个方法都是type类中的方法,在这里我们继承type类后重载这两个方法等于覆盖了type类中的这两个方法。有了这个直观的感受我们接着进一步探索类对象的创建过程。

一个类在声明了元类之后(就是类名后面加个括号,里面写着metaclass=XXX)。当程序运行时,在class语句的末尾就会自动创建类对象。假设我们有一个demo类,声明它的元类为Meta,那么在demo的class语句完结后紧接着执行一句:

demo=Meta(name,bases,dict)

传入三个参数,第一个是demo的类名称(字符串类型),第二个是demo类的父类元组,第三个是demo类的类字典。这时候就需要关注Meta了,Meta也是一个类,它是type的子类,同时type也是Meta的元类,就是说Meta类对象是type的实例。在type类中有一个__call__方法,这个方法是一个运算符重载方法,拦截type(xxx,xxx,xxx,...)这样的调用。回到刚才的

demo=Meta(name,bases,dict)

由于Meta是type的实例,因此当这样的调用形式出现时必然会触发type中的__call__方法。由于Meta是type的实例,因此在传参的时候除了刚刚写出的三个参数外还会自动传入一个Meta自己,因此type的__call__方法实际上会接收到四个参数。现在程序运行到了type的__call__方法中,在这个方法中的调用过程我们用这样的一段伪代码来展示:



正如同代码中所展示的,在__call__方法中首先调用元类的__new__方法得到一个类对象,再把这个类对象传入元类的__init__方法对这个类对象进行初始化最后再返回这个类对象,这也印证了最初我们的论断。元类构造一个类对象基本上就是这么一个过程。

一个例子

为了更加说明元类构造类对象的过程,我从书上找了一个例子改了一下贴在这:



这个例子是为了说明元类构建类对象时__call__方法的调用。首先梳理一下程序的结构:Eggs和Spam是两个常规的类,其中Spam是Eggs的子类,Spam的元类为SubMeta,而SubMeta的元类又为SuperMeta。之所以要绕这么一下就是为了让元类本身的构造过程也暴露出来。

接下来分析一下程序的运行。首先要构建Spam就要先构建SubMeta。SubMeta的元类为SuperMeta,在SuperMeta中定义了__init__和__new__方法,这是定义元类中一个比较常规的做法就不说了,我们要关注一下SuperMeta中定义的__call__方法并关注这个方法执行的时机,这很重要。首先,构建SubMeta等于执行这样的语句:

SubMeta=SuperMeta(name,bases,dict)

但这是否意味着SuperMeta的__call__会执行呢?答案是否定的,因为从原理上来说SuperMeta是type的实例,因此上面的调用会执行type中的__call__方法而不是SuperMeta中的。接着,由于SuperMeta中重载了type的__new__和__init__方法,因此type类中的__call__会调用SuperMeta的这两个方法,调用之后SubMeta类对象就构建完成了,注意此时的SubMeta是SuperMeta的类实例,明白这点很重要。接下来就要构建Spam类对象:

Spam=SubMeta(name,bases,dict)

由于SubMeta是SuperMeta的实例,因此上面代码的调用会触发SuperMeta的__call__方法,就是我们刚刚提到的那个。接着会调用SubMeta类中的__init__和__new__方法,如果SubMeta中这两个方法找不到或者没找全,程序就会顺着继承树找type类中的对应方法。

那么我们预测一下输出吧,首先肯定是SuperMeta的__new__和__init__执行,然后是SuperMeta的__call__执行,接着是SubMeta的__new__和__init__执行:



结果很显然,印证了之前的预测。

在这里,我们还可以继续思考一下。此处的Spam是SubMeta的实例,如果在SubMeta中定义一个__call__方法,那么当Spam正常创建实例的时候会发生什么呢?这个问题就暗示了python创建实例背后的故事,其实这个过程和类对象的构建过程非常相似甚至代码都是一模一样的,同样也是触发__call__方法,进而调用__new__分配内存,然后调用__init__做一些初始化的工作。只不过和类对象不同的是类对象创建中__new__返回的是一个类,而实例创建中返回的是一个实例。之所以会有这样的区别在于在__call__方法中__new__和__init__的调用是取决于__call__的第一个参数,上面的例子中即为Spam,而Spam不是type的子类,因此Spam的__init__和__new__方法指向的是他自己或者是object的对应方法,因此才出现了__new__方法返回结果不同的现象。

写在最后

好啦,元类基本的东西差不多就是这样了,至于具体应用还是蛮多的比如给类动态添加方法,模拟实现java中接口的特性等等。这都需要自己慢慢探索实践。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,517评论 18 139
  • 元类允许我们拦截并扩展类创建----提供了一个API以插入在一条class语句结束时运行的二维逻辑,尽管是以与装饰...
    低吟浅唱1990阅读 168评论 0 0
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,672评论 0 9
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,514评论 18 399