网上关于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__
。