Python中metaclass学习记录

网上关于metaclass的文章很多,内容也相当全面生动。在这里用学习记录的方式尝试自己复述一遍,加深理解。如有错漏,还请包涵指教。
  python中我们由类实例化得到对象,在python中,类同样也是一个对象,它也是实例化的结果。
  直观一点,像这样:

metaclass()=class
class()=object

网上一些经典的讲解,从这个概念直接跳到了type的使用上,接着就是一些应用化的东西了。在这里我自己总结了一下python中创建一个类的流程。为python中元类“是什么”到“能做什么”间架起一道桥梁。
  首先我们用type来创建一个类:

type(name,bases,attr)#其中name为类名,bases为一个tuple,表示继承关系,attr为包含了类属性的dict

我们将它用一个函数包起来:

def meta(name,bases,attr):
  print(name,bases,attr)
  return type(name,bases,attr)

在一个类中指定其metaclass为上面的meta函数:

class A(metaclass=meta):
  pass

运行一下,注意这里并没有A(),但仍然有print的结果。说明当使用class关键字时,python会自动寻找metaclass(如果没有指定则使用默认的type),并为其传入三个参数。虽然概念上讲,能实例化得到一个类的,都能称为元类。这是广义上的元类。
  但python中从元类-->类-->实例,里面有些步骤是由python自动完成的,不遵循它的规则,要么报错,要么最后没有想要的结果。因此在python中元类的使用基本上基于type(即python中的默认元类)及其子类。
  回到type上来,前面我们用type(name,bases,attr)生成了类,但千万不要将其认为是一个函数。抛开元类的概念,把它当成一个类一样使用。用一个类继承它并为其加入新的方法和属性。

class meta(type):
  foo='foo'
  def __init__(self,name,bases,attr):#继承自type类,初始化参数也和type一样。
    self.hi='hello'
  def hello(self):
    print(self.hi)
meta('foo',(),{}).hello()#输出“hello”

注意这里的meta('foo',(),{})结果是一个类,并不是类的实例。同时meta('foo',(),{})上拥有类属性bar、实例属性hi、和实例方法hello(这里的类指元类,实例即元类的实例)。实例方法hello是不是很像@staticmethod?从结果上看是这样,都获得了一个类,在这个类在还未实例化时,就可以调用方法。究其原因,@staticmethod是一种功能上的实现,而使用元类达到这种效果时,则是利用了“实例会带有类中定义的属性和方法,而类是元类的实例”这一语言的特性。
  值得注意的是:无论是元类的类属性“foo”还是元类的实例属性“hi”和实例方法“hello”,在元类实例的实例(也就是我们最后得到的对象)中并不存在(这一点与下面会提到的在type参数内传入属性或方法,得到的效果不一样)。具体看代码:

class meta(type):
  bar='bar'
  def __init__(self,name,bases,attr):#继承自type类,初始化参数也和type一样。
    self.hi='hello'
  def hello(self):
    print(self.hi)
meta('foo',(),{})().hello()#注意这里多了一对括号,结果会报错,因为元类实例的实例中没有hello方法。

而如果使用@staticmethod,类实例化之后依然可以调用类方法:

class A:
  @staticmethond
  def hello():
    print('hello')
A.hello()
A().hello()#两者均可输出“hello”

再看type,type(name,bases,attr)中第三个参数也能传入方法和属性,那么这些方法和属性又会在哪里出现?

meta=type('meta',(),{'foo':'foo'})
A=meta()
B=meta()
print(A.foo,B.foo)#输出“foo foo”
meta.foo='bar'
print(A.foo,B.foo)#输出“bar bar”

上面表明了在type中第三个参数定义的属性为类属性而非实例属性。
  但是在其中传入的方法则会变成实例方法,这里就不多演示了。
</br>
  总结一下:
  第一点,使用“class A(metaclass=用户自定义的元类)”这样的语句时,python会把classA中定义的属性和方法传入指定元类的第三个参数中。
  第二点,元类中定义的属性和方法,虽然在元类的实例(类)中可以使用,但在元类实例的实例(对象)中是没有的。
  由于第一点的存在,在元类里添加东西似乎对最终的对象没什么影响。
  而第二点说明python会自动为你传递参数,效果和使用type(name,bases,attr)没什么区别,并且后者既麻烦也不直观。
  因此元类虽然在对象生成链的上游,但并不能满足“越靠近源头越强大”的愿望。
  不过我们依然可以用元类做一些事,不难想到,我们可以拦截python自动传给type的参数,动态地为其添加一些东西。

def meta(name,bases,attr):
  attr['hello']='hello'
  return type(name,bases,attr)
class A(metaclass=meta):
  pass
print(A().hello)#输出hello

果然,成功输出了“hello”。这就是元类最基本的用法,让我们改写一下,不用函数包装,而是用类继承的方式。

class meta(type):
  def __new__(cls,name,bases,attr):
    attr['hello']='hello'
    return type(name,bases,attr)#注意这一行!
class A(metaclass=meta):
  pass
print(A().hello)#输出hello

看上面的注释“#注意这一行”,这里我用的是type,而不是type.__new__,虽然在例子中,它们的效果一样,但在其他情况会出现一点小坑。
  了解python中__new__的朋友们知道:__new__必须返回由其父类__new__方法实例化的当前类,否则当前类的__init__方法是不会被调用的。如果__new__返回其他东西(包括但不限于其他类的实例,几乎可以是任何东西),类实例化的结果会直接指向__new__的返回值。具体看代码:

class A:
  def __new__(cls):
    return 'I'm not a class'
print(A())#输出“I'm not a class”

因此之前我们如果使用的是type,会出现“狸猫换太子”的情况。看代码:

class meta(type):
  foo='foo'
  def __new__(cls,name,bases,attr):
    return type(name,bases,attr)
class A(metaclass=meta):
  pass
print(A.foo)#会报错,因为返回的是一个新的type实例。 

这次我们换成type.__new__来试试。

class meta(type):
  foo='foo'
  def __new__(cls,name,bases,attr):
    return type.__new__(cls,name,bases,attr)#注意换成了type.__new__
class A(metaclass=meta):
  pass
print(A.foo)#正确返回!

欸,我们前面不是已经有了结论,在元类里定义属性和方法,都不出现在最终的对象上。我们是面向对象编程,最后反正使用的是对象,用type还是type.__new__,似乎没什么区别啊?
  这里就要请出我们的__call__方法了,坑也就是在这里出现的。
  我们知道,在类中定义__call__方法,会让类如同函数一样可以调用,那么在元类中定义__call__会发生什么?

class meta(type):
  def __call__(self):
    print('hello')
class A(metaclass=meta):
  foo='foo'
A()#输出“hello”
print(A().foo)#报错,显示A()的类型为“NoneType”

可见如果我们在元类中定义__call__方法,__call__方法会覆盖原来的__call__,也就是实例化!
  下面我们看一个用__call__方法实现的单例。

class single(type):
  def __call__(self):
    if not hasattr(self,'_instance'):
      self._instance=super().__call__()#注意这里
    return self._instance
class A(metaclass=single):
  pass

注意其中的super().__call__(),因为我们重写了single的__call__,它的实例已经不能正确地返回原来的东西了(原来返回的是对象)。这时需要调用父类即type的__call__方法,super()会自动帮我们找到继承链的上一个类,并把当前的self作为参数传入调用的方法中。父类的__call__会返回正常的结果(这里的self是元类的实例——,元类中定义的__call__在元类实例化的结果——被调用时生效,类中定义的__call__在类实例化的结果——对象被调用时生效!)。
  我们的单例依赖于重写元类传给类的__call__方法,如果修改元类的__new__方法,此时一定要记得返回super().__new__(cls,name,bases,attr)或者type.__new__(cls,name,bases,attr)!不要直接使用type(name,bases,attr)这种方式,会出现“狸猫换太子”!生成的类中不存在__call__

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 写在前面 这两天仔细研究了python中元类的概念,从最开始的一头雾水,到现在的渐渐有一点明白。想借这篇文章来阐述...
    光的文明阅读 439评论 0 0
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • python的函数参数传递 看两个例子: 所有变量都可以理解为内存中一个对象的“引用”,或者,可以看做C中的vio...
    marvinxu阅读 5,838评论 2 30
  • 互联网时代,知识的获取从未如现在这般简单。而量的暴增,并不等同于质的提升。在当前信息过载的情况下,如何在庞杂的、鱼...
    xihe11阅读 889评论 1 0